Design¶
Types¶
signature :: [type]
a list of types
Dispatcher :: {signature: function}
A mapping of type signatures to function implementations
namespace :: {str: Dispatcher}
A mapping from function names, like 'add', to Dispatchers
Dispatchers¶
A Dispatcher
object stores and selects between different
implementations of the same abstract operation. It selects the
appropriate implementation based on a signature, or list of types. We
build one dispatcher per abstract operation.
f = Dispatcher('f')
At the lowest level we build normal Python functions and then add them
to the Dispatcher
.
>>> def inc(x):
... return x + 1
>>> def dec(x):
... return x - 1
>>> f.add((int,), inc) # f increments integers
>>> f.add((float,), dec) # f decrements floats
>>> f(1)
2
>>> f(1.0)
0.0
Internally Dispatcher.dispatch
selects the function implementation.
>>> f.dispatch(int)
<function __main__.inc>
>>> f.dispatch(float)
<function __main__.dec>
For notational convenience dispatchers leverage Python’s decorator syntax to register functions as we define them.
f = Dispatcher('f')
@f.register(int)
def inc(x):
return x + 1
@f.register(float)
def dec(x):
return x - 1
This is equivalent to the form above.
It adheres to the standard implemented by functools.singledispatch
in
Python 3.4 (although the “functional form” of register
is not supported).
As in singledispatch
, the register
decorator returns the
undecorated function, which enables decorator stacking.
@f.register(str)
@f.register(tuple)
def rev(x):
return x[::-1]
The Dispatcher creates a detailed docstring automatically.
To add a description of the multimethod itself,
provide it when creating the Dispatcher
.
>>> f = Dispatcher('f', doc="Do something to the argument")
>>> @f.register(int)
... def inc(x):
... "Integers are incremented"
... return x + 1
>>> @f.register(float)
... def dec(x):
... "Floats are decremented"
... return x - 1
>>> @f.register(str)
... @f.register(tuple)
... def rev(x):
... # no docstring
... return x[::-1]
>>> print(f.__doc__)
Multiply dispatched method: f
Do something to the argument
Inputs: <float>
----------------
Floats are decremented
Inputs: <int>
--------------
Integers are incremented
Other signatures:
str
tuple
Namespaces and dispatch
¶
The dispatch
decorator hides the creation and manipulation of
Dispatcher
objects from the user.
# f = Dispatcher('f') # no need to create Dispatcher ahead of time
@dispatch(int)
def f(x):
return x + 1
@dispatch(float)
def f(x):
return x - 1
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.
# This creates and stores a new Dispatcher('g')
# namespace['g'] = Dispatcher('g')
# namespace['g'].add((int,), g)
@dispatch(int)
def g(x):
return x ** 2
We store this new Dispatcher
in a namespace. A namespace is simply
a dictionary that maps function names like 'g'
to dispatcher objects
like Dispatcher('g')
.
By default dispatch
uses the global namespace in
multipledispatch.core.global_namespace
. If several projects use this
global namespace unwisely then conflicts may arise, causing difficult to
track down bugs. Users who desire additional security may establish
their own namespaces simply by creating a dictionary.
my_namespace = dict()
@dispatch(int, namespace=my_namespace)
def f(x):
return x + 1
To establish a namespace for an entire project we suggest the use of
functools.partial
to bind the new namespace to the dispatch
decorator.
from multipledispatch import dispatch
from functools import partial
my_namespace = dict()
dispatch = partial(dispatch, namespace=my_namespace)
@dispatch(int) # Uses my_namespace rather than the global namespace
def f(x):
return x + 1