Multimethods in Python

python

Clojure-styled multimethods can be easily added to Python using the Multiple Dispatch technique.

So, PEP-443 aka Single-dispatch generic functions has made it into Python. There is a nice writeup of the singledispatch package features by Łukasz Langa.

Although I’m glad that Python is evolving in the right direction, I can’t see how single dispatch alone could be enough. In essence, PEP-443 defines a way of dynamically extending existing types with externally defined generic functions. Which is nice, but too limited.

What is really interesting is multiple dispatch. There are a few packages bringing multimethods to Python; all of them are overcomplicated to my taste.

Here’s my take on it. I will not talk much, better show you the code.

This is the complete implementation:

# multidispatch.py

import operator
from collections import OrderedDict

class DuplicateCondition(Exception): pass

class NoMatchingMethod(Exception): pass

class defmulti(object):
    def __init__(self, predicate):
        self.registry = OrderedDict()
        self.predicate = predicate

    def __call__(self, *args, **kw):
        method = self.dispatch(*args, **kw)
        return method(*args, **kw)

    def dispatch(self, *args, **kw):
        for condition, method in self.registry.items():
            if self.predicate(condition, *args, **kw):
                return method
        return self.notfound

    def notfound(self, *args, **kw):
        raise NoMatchingMethod()

    def when(self, condition):
        if condition in self.registry:
            raise DuplicateCondition()
        def deco(fn):
            self.registry[condition] = fn
            return fn
        return deco

    def default(self, fn):
        self.notfound = fn
        return fn

    @classmethod
    def typedispatch(cls):
        return cls(lambda type, first, *rest,
                   **kw: isinstance(first, type))

And here’s how to use it:

import types
from multidispatch import defmulti, NoMatchingMethod

# Exhibit A: Dispatch on the type of the first parameter.
#            Equivalent to `singledispatch`.

cupcakes = defmulti.typedispatch()

@cupcakes.when(types.StringType)
def str_cupcakes(ingredient):
    return "Delicious {0} cupcakes".format(ingredient)

@cupcakes.when(types.IntType)
def int_cupcakes(number):
    return "Integer cupcakes, anyone? I've got {0} of them.".format(number)

@cupcakes.default
def any_cupcakes(thing):
    return ("You can make cupcakes out of ANYTHING! "
            "Even out of {0}!").format(thing)

print cupcakes("bacon")
print cupcakes(4)
print cupcakes(cupcakes)

# Exhibit B: dispatch on the number of args, no default

@defmulti
def jolly(num, *args):
    return len(args) == num

@jolly.when(1)
def single(a):
    return "For {0}'s a jolly old fellow!".format(a)

@jolly.when(2)
def couple(a, b):
    return "{0} and {1} are such a jolly couple!".format(a, b)

print jolly("Lukasz")
print jolly("Fish", "Chips")
try:
    jolly("Good", "Bad", "Ugly")
except NoMatchingMethod:
    print "Noo! Angel Eyes!"