Avoiding Repetitive Methods

We all know that you should avoid writing the same code over and over: put it in a function and just call that function whenever you need it. But what if what you need is a class with a whole bunch of really similar methods, each with a different name, but almost identical behaviour (just differing by a constant, say)?

As a really dumb example:

class A:

    def function_a(self):
        return 'a'

    def function_b(self):
        return 'b'

    def function_c(self):
        return 'c'

    ...

[Obviously, you wouldn't really have methods quite this simple, but this is just a simplified version of any case where the method name provides a constant for some sort of calculation.]

In our example case, you should be able to call the methods as follows:

>> a = A()

>> a.function_a()
'a'

>> a.function_b()
'b'

>> a.function_c()
'c'

...

It all works fine as defined above, but we could have potentially dozens of methods that all do basically the same thing. What we really want here is a way to say, "If we call a method named 'function_' followed by some character, return that character."

Luckily, we can do this by defining __getattr__ for the class. The class method names are treats as attributes, like any other. And __getattr__ will be called for any undefined attributes.

Naively, we might try something like this:

class A:

    def __getattr__(self, attr):
        if attr[0:9] == 'function_':
            return attr[9:]

But this doesn't quite do what we want. This would be fine if we wanted to have a bunch of class variables named function_a, function_b, etc. But we actually wanted class methods. The difference is clear when we try to run it:

>> a = A()

>> a.function_a
'a'

>> a.function_a()
TypeError: 'str' object is not callable

Rather than returning a value, we need a way to return a callable object (that will produce that value when called). Luckily, the functools.partial can do this for us. It takes a function name, and some number of arguments, and returns a callable object with the provided arguments already included.

import functools

class A:

    def __getattr__(self, attr):
        if attr[0:9] == 'function_':
            return functools.partial(self.function_char, attr[9:])

    def function_char(self, char):
        return char

In this case, rather than returning the character from the method name directly, we bake it into another method, which can be called (without needing to supply any more arguments):

>> a = A()

>> a.function_a()
'a'

>> a.function_a
<functools.partial object at [address]>

In this case, we're defining methods without arguments, but the same strategy could be used for arguments. functools.partial can be used to bake-in only some of the arguments for a function, while the rest are still provided when it is called.

For example, suppose, rather than just printing the character, we want function_a, function_b, etc. to take an integer argument of the number of times to print the character.

>> a.function_a(1)
'a'

>> a.function_a(5)
'aaaaa'

We just have to change the definition of function_char to take this extra variable:

def function_char(self, char, n):
    return n*char

And all our methods function_a, function_b, etc. now take an integer argument, with no change to __getattr__ needed.

social