Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I use functools.singledispatch with instance methods?

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?

like image 448
Dustin Oprea Avatar asked Jul 07 '14 00:07

Dustin Oprea


People also ask

How do you use Functools in Python?

It can be used in key functions such as sorted(), min(), max(). It applies a function of two arguments repeatedly on the elements of a sequence so as to reduce the sequence to a single value. For example, reduce(lambda x, y: x^y, [1, 2, 3, 4]) calculates (((1^2)^3)^4) .

Which function is imported from Functools module?

from functools import lru_cache, singledispatch.

What is Functools function in Python?

The functools module in Python deals with higher order functions, that is, functions operating on(taking as arguments) or returning functions and other such callable objects. The functools module provides a wide array of methods such as cached_property(func), cmp_to_key(func), lru_cache(func), wraps(func), etc.

Is Functools a standard Python library?

Introduction. The functools module, part of Python's standard Library, provides useful features that make it easier to work with high order functions (a function that returns a function or takes another function as an argument ).


2 Answers

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] 
like image 176
Zero Piraeus Avatar answered Sep 24 '22 18:09

Zero Piraeus


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) 
like image 20
Nuno André Avatar answered Sep 24 '22 18:09

Nuno André