Skip to content

Instantly share code, notes, and snippets.

@adamnew123456
Last active May 15, 2020 15:37
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save adamnew123456/9218f99ba35da225ca11 to your computer and use it in GitHub Desktop.
Save adamnew123456/9218f99ba35da225ca11 to your computer and use it in GitHub Desktop.
dispatchmethod - Using functools.singledispatch For Methods
# This is free and unencumbered software released into the public domain.
#
# Anyone is free to copy, modify, publish, use, compile, sell, or
# distribute this software, either in source code form or as a compiled
# binary, for any purpose, commercial or non-commercial, and by any
# means.
#
# In jurisdictions that recognize copyright laws, the author or authors
# of this software dedicate any and all copyright interest in the
# software to the public domain. We make this dedication for the benefit
# of the public at large and to the detriment of our heirs and
# successors. We intend this dedication to be an overt act of
# relinquishment in perpetuity of all present and future rights to this
# software under copyright law.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
# For more information, please refer to <http://unlicense.org/>
from collections import namedtuple
from functools import singledispatch, update_wrapper
A = namedtuple('A', ['x'])
B = namedtuple('B', ['y'])
C = namedtuple('C', ['z'])
def dispatchmethod(func):
"""
This provides for a way to use ``functools.singledispatch`` inside of a class. It has the same
basic interface that ``singledispatch`` does:
>>> class A:
... @dispatchmethod
... def handle_message(self, message):
... # Fallback code...
... pass
... @handle_message.register(int)
... def _(self, message):
... # Special int handling code...
... pass
...
>>> a = A()
>>> a.handle_message(42)
# Runs "Special int handling code..."
Note that using ``singledispatch`` in these cases is impossible, since it tries to dispatch
on the ``self`` argument, not the ``message`` argument. This is technically a double
dispatch, since both the type of ``self`` and the type of the second argument are used to
determine what function to call - for example:
>>> class A:
... @dispatchmethod
... def handle_message(self, message):
... print('A other', message)
... pass
... @handle_message.register(int)
... def _(self, message):
... print('A int', message)
... pass
...
>>> class B:
... @dispatchmethod
... def handle_message(self, message):
... print('B other', message)
... pass
... @handle_message.register(int)
... def _(self, message):
... print('B int', message)
...
>>> def do_stuff(A_or_B):
... A_or_B.handle_message(42)
... A_or_B.handle_message('not an int')
On one hand, either the ``dispatchmethod`` defined in ``A`` or ``B`` is used depending
upon what object one passes to ``do_stuff()``, but on the other hand, ``do_stuff()``
causes different versions of the dispatchmethod (found in either ``A`` or ``B``)
to be called (both the fallback and the ``int`` versions are implicitly called).
Note that this should be fully compatable with ``singledispatch`` in any other respects
(that is, it exposes the same attributes and methods).
"""
dispatcher = singledispatch(func)
def register(type, func=None):
if func is not None:
return dispatcher.register(type, func)
else:
def _register(func):
return dispatcher.register(type)(func)
return _register
def dispatch(type):
return dispatcher.dispatch(type)
def wrapper(inst, dispatch_data, *args, **kwargs):
cls = type(dispatch_data)
impl = dispatch(cls)
return impl(inst, dispatch_data, *args, **kwargs)
wrapper.register = register
wrapper.dispatch = dispatch
wrapper.registry = dispatcher.registry
wrapper._clear_cache = dispatcher._clear_cache
update_wrapper(wrapper, func)
return wrapper
class Foo:
@dispatchmethod
def handle_message(self, message):
print('Unknown message:', message)
@handle_message.register(A)
def _(self, message):
print('A:', message.x)
@handle_message.register(B)
def _(self, message):
print('B:', message.y)
@handle_message.register(C)
def _(self, message):
print('C:', message.z)
x = Foo()
x.handle_message(12)
x.handle_message(A('a'))
x.handle_message(B('b'))
x.handle_message(C('c'))
@abenkovskii
Copy link

Technically this isn't fully compatible with singledispatch from the standard library. The original one has a 2-argument form of register
https://docs.python.org/3/library/functools.html#functools.singledispatch:

To enable registering lambdas and pre-existing functions, the register() attribute can be used in a functional form:

>> def nothing(arg, verbose=False):
...     print("Nothing.")
...
>> fun.register(type(None), nothing)

@tim-mitchell
Copy link

I wish I had seen this before I wrote methoddispatch!! https://github.com/tim-mitchell/methoddispatch

@DShivansh
Copy link

DShivansh commented Mar 30, 2020

Can anyone please help, I am unable to understand the convention of using functionName.someVariable(it is like assigning to the variable names of the class but instead we are using function name) name like wrapper.register. I also did not understand what does dispatcher._clear_cache do I also couldn't find anything related to it in the documentation?

@adamnew123456
Copy link
Author

@DShivansh

I am unable to understand the convention of using functionName.someVariable(it is like assigning to the variable names of the class but instead we are using function name) name like wrapper.register

Functions are objects like anything else and have their own bag of attributes. The defaults are all __methods__ since they're defined by the runtime:

>>> def f(a: int, b: str) -> float: pass
...
>>>  dir(f)
[ '__annotations__', '__call__', ...]
>>> f.__annotations__
{'a': <class 'int'>, 'b': <class 'str'>, 'return': <class 'float'>}
>>> f.__call__(1, 2) # Identical to f(1, 2)
>>> type(f)
<class 'function'>

All something like wrapper.register does it is add our own properties to that bag:

>>> f.x = 2
>>> f.x
2

You can't go too far with this since there are certain things hardcoded into the function type (like the behavior of most of the __methods__) but you can hang data off the function itself with no problems.

The usual alternative to this would be to define a callable class, which behaves the same way but makes it more obvious what's going on:

>>> class MyFuncWrapper:
...     def __init__(self, f, x):
...         self._function = f
...         self.x = x
...     def __call__(self, *args, **kwargs):
...        return self._function(*args, **kwargs)
...
>>> f = MyFuncWrapper(f, 2)
>>> f.x
2
>>> f(1, 2) # Equivalent to f.__call__(1, 2)

I also did not understand what does dispatcher._clear_cache do I also couldn't find anything related to it in the documentation?

It's an undocumented function defined in the functools module.

Internally singledispatch is aware of "subclasses" created via the abc module, which may dynamically change at runtime. Since that affects what function singledispatch might call, it has to probe abc's subclass cache and see if there have been any changes. If there have then it drops the current dispatch cache and recomputes it so that the wrong function isn't called.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment