How can I use functools.singledispatch with instance methods?

Dustin Oprea picture Dustin Oprea · Jul 7, 2014 · Viewed 13.7k times · Source

Python 3.4 added the ability to define function overloading with static methods. This is essentially the example from the documentation:

from functools import singledispatch


class TestClass(object):
    @singledispatch
    def test_method(arg, verbose=False):
        if verbose:
            print("Let me just say,", end=" ")

        print(arg)

    @test_method.register(int)
    def _(arg):
        print("Strength in numbers, eh?", end=" ")
        print(arg)

    @test_method.register(list)
    def _(arg):
        print("Enumerate this:")

        for i, elem in enumerate(arg):
            print(i, elem)

if __name__ == '__main__':
    TestClass.test_method(55555)
    TestClass.test_method([33, 22, 11])

In its purest form, the singledispatch implementation relies on the first argument to identify type, therefore making it tricky to extend this functionality to instance methods.

Does anyone have any advice for how to use (or jerry-rig) this functionality to get it to work with instance methods?

Answer

Zero Piraeus picture Zero Piraeus · Jul 7, 2014

Update: As of Python 3.8, functools.singledispatchmethod allows single dispatch on methods, classmethods, abstractmethods, and staticmethods.

For older Python versions, see the rest of this answer.

Looking at the source for singledispatch, we can see that the decorator returns a function wrapper(), which selects a function to call from those registered based on the type of args[0] ...

    def wrapper(*args, **kw):
        return dispatch(args[0].__class__)(*args, **kw)

... which is fine for a regular function, but not much use for an instance method, whose first argument is always going to be self.

We can, however, write a new decorator methdispatch, which relies on singledispatch to do the heavy lifting, but instead returns a wrapper function that selects which registered function to call based on the type of args[1]:

from functools import singledispatch, update_wrapper

def methdispatch(func):
    dispatcher = singledispatch(func)
    def wrapper(*args, **kw):
        return dispatcher.dispatch(args[1].__class__)(*args, **kw)
    wrapper.register = dispatcher.register
    update_wrapper(wrapper, func)
    return wrapper

Here's a simple example of the decorator in use:

class Patchwork(object):

    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

    @methdispatch
    def get(self, arg):
        return getattr(self, arg, None)

    @get.register(list)
    def _(self, arg):
        return [self.get(x) for x in arg]

Notice that both the decorated get() method and the method registered to list have an initial self argument as usual.

Testing the Patchwork class:

>>> pw = Patchwork(a=1, b=2, c=3)
>>> pw.get("b")
2
>>> pw.get(["a", "c"])
[1, 3]