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\$
  • \$\begingroup\$ Obligatory suggestion to follow the Python style guide. \$\endgroup\$ – jonrsharpe Jul 14 '14 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 Jul 15 '14 at 9:57
  • \$\begingroup\$ Neither of your classes are named in CapitalizedWords. \$\endgroup\$ – jonrsharpe Jul 15 '14 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 Jul 15 '14 at 10:06
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\$
  • \$\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 Aug 5 '14 at 12:24
  • \$\begingroup\$ @Joschua Yeah, I assume the (object) thing is a matter of taste. I prefer to omit it though. \$\endgroup\$ – Jonas Schäfer Aug 5 '14 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\$ – Jonas Schäfer Aug 12 '14 at 11:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.