Multiple argument dispatching.
This module was inspired on Aric Coady’s multimethod module in pypi.
Multiple argument dispatch is a technique in which different implementations of a function can be used depending on the type and number of arguments. This module provides the multifunction and multimethod classes that provides multiple argument dispatch to Python. Multiple dispatch functions can be easily created by decorating an ordinary python function:
@multifunction(*types)
def func(*args):
pass
func is now a multifunction which will delegate to the above implementation when called with arguments of the specified types. If an exact match can’t be found, the next closest method is called (and cached). If there are multiple or no candidate methods, a DispatchError (which derives from TypeError) is raised.
The easiest way to create a new multifunction is to supplement an implementation with the multifunction(*types) decorator.
>>> @multifunction(int, int)
... def join(x, y):
... "Join two objects, x and y"
... return x + y
This creates a multiple dispatch function that only accepts two integer positional arguments.
>>> join(1, 1)
2
>>> join(1., 1.)
Traceback (most recent call last):
...
DispatchError: join(float, float): no methods found
An alternative way to create a multifunction object is to use the explicit multifunction.new() constructor.
>>> join = multifunction.new('join', doc="Join two objects, x and y")
After creating a multifunction instance, types can be mapped to their respective implementations functions by assigning keys to a dictionary.
>>> join[int, int] = lambda x, y: x + y
>>> join[float, float] = lambda x, y: x + y + 0.1
The join multifunction now accepts (int, int) and (float, float) arguments
>>> join(1, 1) + join(1., 1.) # 2 + 2.1
4.1
The .dispatch() method is a more convenient way to add additional implementations to the multifunction.
>>> @join.dispatch(list, list)
... def join(x, y):
... return x + y
>>> join([1], [join(1, 1)])
[1, 2]
Multiple decorators are also accepted.
>>> @join.dispatch(float, int)
... @join.dispatch(int, float)
... def join_numbers(x, y):
... return x + y
If the implementation function have a different name than the multifunction, both will live in the same namespace.
>>> join(1, 2.), join_numbers(1 + 2j, 2 + 1j)
(3.0, (3+3j))
A multifunction is a callable dictionary that maps tuples of types to functions. All methods of regular dictionaries are accepted.
>>> dict(join)
{(<class '...'>, <class '...'>): <function ... at 0x...>, ...}
We can remove an implementation by simply deleting it from the dictionary.
>>> del join[list, list]
>>> join([1], [2])
Traceback (most recent call last):
...
DispatchError: join(list, list): no methods found
Use None to capture arbitrary types at a given position. The caller always tries to find the most specialized implementation, and uses None only as a fallback.
>>> @join.dispatch(None, int)
... def join(x, y):
... return x + y - 0.1
>>> join(1, 2), join(1 + 0j, 2)
(3, (2.9+0j))
One can also define a fallback function that is called with an arbitrary number of positional arguments. Just use the fallback decorator of a multifunction
>>> @join.fallback
... def join(*args):
... return sum(args)
>>> join(1, 2, 3, 4)
10
multifunction‘s can be used inside class declarations as any regular function. It has a small inconvenient that the type of first argument self must be present in the type signature. Since the class was not yet created, the type does not exist and the fallback None must be used.
>>> class Foo(object):
... @multifunction(None, int)
... def double(self, x):
... print(2 * x)
...
... @double.dispatch(None, complex)
... def double(self, x):
... print("sorry, I don't like complex numbers")
The multiple argument dispatch works as before.
>>> Foo().double(2)
4
>>> Foo().double(2j)
sorry, I don't like complex numbers
The multimethod class omits the type declaration for the first argument of the function (usually the first argument is self). It is convenient for using in the body of class declarations.
>>> class Bar(object):
... @multimethod(int) # the type of self is not declared...
... def double(self, x): # ...but it appears on implementations as usual
... print(2 * x)
...
... @double.dispatch(complex)
... def double(self, x):
... print("sorry, I don't like complex numbers")
>>> Bar().double(2)
4
>>> Bar().double(2j)
sorry, I don't like complex numbers
multifunction(None, ...) and multimethod(...) are equivalent. The second is just a small convenience that helps code look clean and tidy.