9
\$\begingroup\$

This is based on my first review and the suggested points in "Multiple dispatch decorator in Python":

import inspect
from functools import wraps, update_wrapper, partial
class multi_dispatch(object):
 """
 Returns a multiple dispatch version of the function, that has a "register"
 method with which new versions of the function can be registered. These
 functions each must have annotated parameters with the types they shall be
 called with. Keyword parameters and "self" must not be annotated.
 The decorated function works by searching for an exact match for the types
 of the given arguments. If no match is found, the function on which this
 decorator has been used, is called. If you don't want this, raise a
 TypeError inside the function.
 >>> FMT = 'In {} the {} and {} collide.'
 >>> FMT_SA = 'In {} the {} is smashed to bits by {}.'
 >>> FMT_AS = 'In {} the {} is hit by the {}.'
 >>>
 >>> class GameObject(object):
 ... def __init__(self, name):
 ... self.name = name
 ...
 ... def __repr__(self):
 ... return self.name
 ...
 ...
 >>> class Asteroid(GameObject):
 ... pass
 ...
 ...
 >>> class Spaceship(GameObject):
 ... pass
 ...
 ...
 >>> class Starcluster(GameObject):
 ... @multi_dispatch
 ... def collide(self, a, b):
 ... print(FMT.format(self, a, b))
 ...
 ... @collide.register
 ... def collide(self, a: Spaceship, b: Asteroid):
 ... print(FMT_SA.format(self, a, b))
 ...
 ... @collide.register
 ... def collide(self, a: Asteroid, b: Spaceship):
 ... print(FMT_AS.format(self, a, b))
 ...
 ...
 >>> a = Asteroid('Iris')
 >>> s = Spaceship('Voyager')
 >>> sc = Starcluster('Messier 69')
 >>>
 >>> sc.collide(a, s)
 In Messier 69 the Iris is hit by the Voyager.
 >>> sc.collide(s, 'a grain of dust')
 In Messier 69 the Voyager and a grain of dust collide.
 >>>
 >>>
 >>> @multi_dispatch
 ... def collide(a, b):
 ... print(FMT.format('outer space', a, b))
 ...
 ...
 >>> @collide.register
 ... def collide(a: Spaceship, b: Asteroid):
 ... print(FMT_SA.format('outer space', a, b))
 ...
 ...
 >>> @collide.register
 ... def collide(a: Asteroid, b: Spaceship):
 ... print(FMT_AS.format('outer space', a, b))
 ...
 ...
 >>> collide(a, s)
 In outer space the Iris is hit by the Voyager.
 >>> collide(s, a)
 In outer space the Voyager is smashed to bits by Iris.
 >>> collide(s, 'a grain of dust')
 In outer space the Voyager and a grain of dust collide.
 """
 def __init__(self, function):
 update_wrapper(self, function)
 self.default_function = function
 self._annotation_to_function = {}
 self._is_bound_method = False
 def __get__(self, obj, objtype):
 self._is_bound_method = True
 return partial(self.__call__, obj)
 def register(self, function):
 annotation_to_function = self._annotation_to_function
 annotation = self._get_annotations(function)
 if annotation in annotation_to_function:
 raise TypeError("duplicate registration of: " +
 str(annotation))
 annotation_to_function[annotation] = function
 return self
 def _get_annotations(self, function, empty=inspect.Parameter.empty):
 parameters = inspect.signature(function).parameters
 annotations = (a.annotation for a in parameters.values())
 return tuple(i for i in annotations if i is not empty)
 def __call__(self, *args, **kwargs):
 annotation_to_function = self._annotation_to_function
 types = tuple(type(a) for a in args)
 if self._is_bound_method:
 types = types[1:]
 if types not in annotation_to_function:
 return self.default_function(*args, **kwargs)
 return annotation_to_function[types](*args, **kwargs)
class inheritance_multi_dispatch(multi_dispatch):
 """
 >>> FMT = 'In {} the {} and {} collide.'
 >>> FMT_SA = 'In {} the {} is smashed to bits by {}.'
 >>> FMT_AS = 'In {} the {} is hit by the {}.'
 >>>
 >>> class GameObject(object):
 ... def __init__(self, name):
 ... self.name = name
 ...
 ... def __repr__(self):
 ... return self.name
 ...
 ...
 >>> class Asteroid(GameObject):
 ... pass
 ...
 ...
 >>> class Spaceship(GameObject):
 ... pass
 ...
 ...
 >>> class Starcluster(GameObject):
 ... @inheritance_multi_dispatch
 ... def collide(self, a, b):
 ... print(FMT.format(self, a, b))
 ...
 ... @collide.register
 ... def collide(self, a: (Spaceship, Asteroid), b: str):
 ... print(FMT_SA.format(self, a, b))
 ...
 ... @collide.register
 ... def collide(self, a: Asteroid, b: Spaceship):
 ... print(FMT_AS.format(self, a, b))
 ...
 ...
 >>> a = Asteroid('Iris')
 >>> s = Spaceship('Voyager')
 >>> sc = Starcluster('Messier 69')
 >>>
 >>> sc.collide(a, 'a teapot')
 In Messier 69 the Iris is smashed to bits by a teapot.
 >>> sc.collide(s, 'a teapot')
 In Messier 69 the Voyager is smashed to bits by a teapot.
 >>> sc.collide(a, s)
 In Messier 69 the Iris is hit by the Voyager.
 >>> sc.collide('Moon', 'a stone')
 In Messier 69 the Moon and a stone collide.
 """
 def __init__(self, function):
 self._cache = {}
 self.set_argument_to_annotation_matcher(isinstance)
 super().__init__(function)
 def set_argument_to_annotation_matcher(self, matcher):
 self._matcher = matcher
 def register(self, function):
 self._cache = {}
 return super().register(function)
 def __call__(self, *args, **kwargs):
 cache = self._cache
 types = tuple(type(a) for a in args)
 if self._is_bound_method:
 types = types[1:]
 try:
 method = cache[types]
 except KeyError:
 cache[types] = method = self._find_match(args)
 return method(*args, **kwargs)
 def _find_match(self, args):
 annotation_to_function = self._annotation_to_function
 matcher = self._matcher
 if self._is_bound_method:
 args = args[1:]
 for annotations, function in annotation_to_function.items():
 if all(matcher(arg, ann) for arg, ann in zip(args, annotations)):
 return function
 return self.default_function

Again, I'd like to read what you think about the code from every point of view!

asked Jul 14, 2014 at 16:49
\$\endgroup\$
4
  • \$\begingroup\$ Obligatory suggestion to follow the Python style guide. \$\endgroup\$ Commented Jul 14, 2014 at 17:49
  • 1
    \$\begingroup\$ @jonrsharpe Where did I not follow PEP 8? (I use PyDev with autopep8.py enabled, so it should conform mostly, but it's sadly not perfect yet.) \$\endgroup\$ Commented Jul 15, 2014 at 9:57
  • \$\begingroup\$ Neither of your classes are named in CapitalizedWords. \$\endgroup\$ Commented Jul 15, 2014 at 9:58
  • 1
    \$\begingroup\$ You're right, but naming them lowercase makes more sense in this case, because they're used as a function. (So this an exception, as stated in this answer.) \$\endgroup\$ Commented Jul 15, 2014 at 10:06

1 Answer 1

3
\$\begingroup\$

The code itself looks quite pretty and overall readable, considering that the topic itself requires some knowledge about the Python language internals. Some suggestions in descending order of urgency:

  1. You should either document that @classmethod and @staticmethod are not supported, or implement support for them.

  2. It might be sensible to add a sanity-check in case someone annotates a keyword argument.

  3. I prefer to use formats instead of string concatenation, even for exception messages:

    raise TypeError("duplicate registration of: {}".format(
     annotation))
    

    This allows for easier extension of the error message (in case it turns out to be too shallow at some point; one might want to include the overloaded functions name at some point in the future) and makes it easier to read.

Explicitly inheriting from object is not required anymore in Python 3, but it is not wrong to explicitly specify that.

answered Aug 3, 2014 at 9:48
\$\endgroup\$
3
  • \$\begingroup\$ Thanks. Can you number your points? (That makes it easier to refer to them.) Regarding your third point: I think I prefer explicitly inheriting from object, because 1. Explicit is better than implicit. and 2. similar to your last point, it makes it easier to change in the future in the case that the decorators should extend from another class. \$\endgroup\$ Commented Aug 5, 2014 at 12:24
  • \$\begingroup\$ @Joschua Yeah, I assume the (object) thing is a matter of taste. I prefer to omit it though. \$\endgroup\$ Commented Aug 5, 2014 at 16:29
  • 1
    \$\begingroup\$ @Joschua A recent discussion on python-dev made me rethink your comment and I decided to rephrase that point. \$\endgroup\$ Commented Aug 12, 2014 at 11:58

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.