How can I use functools.singledispatch with instance methods?
Asked Answered
D

3

62

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?

Dom answered 7/7, 2014 at 0:50 Comment(8)
Can you show a contrived example of the kind of thing you're after? IHMO I think circuits would be a better fit for what you want -- Disclaimer: I'm the developer.Araucania
@JamesMills I don't know if I can update the original post with a complete example and still keep this a serious conversation. Maybe something like @test_method.register(type_) (the standard register() command) followed by "def _(self, arg)", maybe made possible by some intermediary bridge function or 'partial' method (functools.partialmethod). I'm hesitant to make any specific guesses since I don't know how register() transforms the original method.Dom
Without knowing more about the problem you're trying to solve in general that's all the advice I can give. AFAIK single-dispatch as introduced in Python 3 was designed for dispatching functions based on the types of arguments passed.Araucania
@JamesMills The problem/desire is being able to get single dispatch on objects (as opposed to classes). I rarely use static methods rather than just reverting plain functions. I could give you an example, but it'd look like every other example of a class with bindable methods in the world with a "register" decorator (I realize that the calling convention of register() requires the base definition to be a static method, so the treatment would have to account for that).Dom
That's why I suggested you look at circuits -- it may be "close enough" to what you need.Araucania
@JamesMills Although I do like your circuits, I'm just looking for whether conventional overloading is possible using native functionality (read: prepackaged packages) since the functionality has now been introduced into Python at some level...Not an event system. Thanks, though.Dom
I like Zero's answer below for the use of decorators, but also check out my answer here: https://mcmap.net/q/55851/-method-overloading-in-python . It shows how to map instance methods to (overloaded) static methods.Gavrah
related: python3: singledispatch in class, how to dispatch self typeBolshevist
D
94

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]
Distrustful answered 7/7, 2014 at 2:37 Comment(2)
Yes! Being, myself, a rather dim individual, I was just wondering if someone could postulate a potential solution using a couple of the existing tools at hand... You know, like with the Apollo 13 mission. Thanks for the PoC.Dom
Hm, interesting, I was looking to see if something like this had been done. A small correction I would make is using update_wrapper(wrapper, dispatcher) rather than func itself. singledispatch uses update_wrapper internally already, so doing this passes up the full interface of singledispatch.Elyssa
F
14

A decorator is essentially a wrapper that takes the wrapped function as an argument and returns another function.

As stated in the accepted answer, singledispatch returns a wrapper that takes the first argument as registered type - self in instance methods.

As shown in that answer, in cases like this you can write another wrapper to monkey patch the decorator. But this kind of hacky fixes are not always the best option.

As with like any other function, you can call the wrapper and pass the arguments to it explicitly, which seems simpler, flatter and more readable to me if this kind of method overloading is only seldom made in a package.

from functools import singledispatch

class TestClass(object):

    def __init__(self):
        self.test_method = singledispatch(self.test_method)
        self.test_method.register(int, self._test_method_int)
        self.test_method.register(list, self._test_method_list)

    def test_method(self, arg, verbose=False):
        if verbose:
            print("Let me just say,", end=" ")

        print(arg)

    def _test_method_int(self, arg):
        print("Strength in numbers, eh?", end=" ")
        print(arg)

    def _test_method_list(self, arg):
        print("Enumerate this:")

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


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

There's another module, multipledispatch (not standard but included in Anaconda and without any non-standard dependencies) that, as the name already indicates and unlike singledispatch, allows multimethods.

In addition to Dispatcher objects, with singledispatch-compatible syntaxis, it provides a dispatch decorator which hides the creation and manipulation of these objects from the user.

The dispatch decorator uses the name of the function to select the appropriate Dispatcher object to which it adds the new signature/function. When it encounters a new function name it creates a new Dispatcher object and stores name/Dispatcher pair in a namespace for future reference.

For instance:

from types import LambdaType
from multipledispatch import dispatch

class TestClass(object):

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

        print(arg)

    @dispatch(int, float)
    def test_method(self, arg, arg2):
        print("Strength in numbers, eh?", end=" ")
        print(arg + arg2)

    @dispatch((list, tuple), LambdaType, type)
    def test_method(self, arg, arg2, arg3):
        print("Enumerate this:")

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


if __name__ == '__main__':

    test = TestClass()
    test.test_method(55555, 9.5)
    test.test_method([33, 22, 11], lambda x: x*2, float)
Fluted answered 28/8, 2017 at 10:27 Comment(0)
C
6

Python 3.8 functools introduced function overloading for instance methods using a new decorator @singledispatchmethod.

According to the docs;

the dispatch happens on the type of the first non-self or non-cls argument.

Therefore the type for the argument that comes immediately after self is the one that triggers a dispatch of the method. Something like below.

from functools import singledispatchmethod


class Cooking:
    @singledispatchmethod
    def cook(self, arg):
        return f"I'm cooking {arg} eggs."

    @cook.register
    def _(self, arg: int):
        return f"I'm cooking {arg} eggs."

    @cook.register
    def _(self, arg: bool):
        return f"Am I cooking eggs? {arg}"

f = Cooking()
print(f.cook('many'))
# I'm cooking many eggs.
print(f.cook(50))
# I'm cooking 50 eggs.
print(f.cook(True))
# Am I cooking eggs? True

Clea answered 23/10, 2022 at 14:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.