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!

\$\endgroup\$
4
  • \$\begingroup\$Obligatory suggestion to follow the Python style guide.\$\endgroup\$CommentedJul 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\$
    – Joschua
    CommentedJul 15, 2014 at 9:57
  • \$\begingroup\$Neither of your classes are named in CapitalizedWords.\$\endgroup\$CommentedJul 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\$
    – Joschua
    CommentedJul 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.

\$\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\$
    – Joschua
    CommentedAug 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\$CommentedAug 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\$CommentedAug 12, 2014 at 11:58

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.