MacPython Logo from __future__ import *

buy music albums Silver Apples buy mp3 albums Tarrus Riley buy tracks mp3 Kravits buy Reaper albums mp3 buy Kravits albums music buy music Evita CD online albums mp3 Silver Apples download Madonna CD music buy tracks music Kravits download music albums Silver Apples

2005-03-30

Five-minute Multimethods In Python (using dispatch)

Filed under: python — bob @ 9:37 pm

While PyProtocols dispatch more or less implements multimethods out of the box, I thought it would probably be useful to demonstrate that Guido's implementation of Five-minute Multimethods in Python can be cloned using it.

from dispatch.strategy import default, Argument, Signature
from dispatch.predicates import Getattr, Pointer
from dispatch.functions import GenericFunction

def multimethod_signature(args, kwargs):
    def isClass((arg, cls)):
        return Getattr(arg, '__class__'), Pointer(cls)
    def parse():
        for i, typ in enumerate(args):
            yield Argument(i), typ
        for name, typ in kwargs.iteritems():
            yield Argument(name=name), typ
    return Signature(map(isClass, parse()))

def multimethod(*args, **kwargs):
    def register(function):
        name = function.__name__
        oldfunc = function.func_globals.get(name, function)
        if not hasattr(oldfunc, 'addMethod'):
            oldfunc = GenericFunction(function).delegate
            def not_applicable(*a, **kw):
                raise TypeError("No Applicable Methods")
            oldfunc.addMethod(default, not_applicable)
        elif oldfunc is function:
            function = getattr(function, '_last_multimethod', function)
        sig = multimethod_signature(args, kwargs)
        oldfunc.addMethod(sig, function)
        oldfunc._last_multimethod = function
        return oldfunc
    return register

@multimethod(int, int)
def foo(a, b):
    print "code for two ints"

@multimethod(float, float)
def foo(a, b):
    print "code for two floats"

@multimethod(unicode, unicode)
@multimethod(str, str)
def foo(a, b):
    print (a, b)
    print "code for two strings"
    print "or unicodes..."

Note that it would be slightly less code if the type arguments were allowed to use isinstance(...), because that's what dispatch wants to do, but I wanted to clone Guido's implementation such that the class of the argument must be equal to the given type.

The other differences from Guido's implementation are:

  • No global registry, the registry is the func globals (could be extended to also look in locals).. so you could use this from multiple modules and not have to worry about having a flat namespace for multimethod names in your interpreter!
  • If you use default arguments, it's going to use the defaults specified on the first multimethod
  • You don't have to specify the type of every argument, you could multimethod dispatch on the first argument, or even named arguments

Note that would likely be more sensible and less code to use isinstance(...) style multimethod dispatch instead, but I just wanted to make sure that the (weird?) semantics were preserved. I'm guessing he used type identity because you could dispatch on a dict, so the implementation would be short :)

The following is a more "natural" implementation of how this would be implemented using PyProtocols dispatch. This version will accept subclasses, and can not be stacked. Its syntax is not as terse as @multimethod, but it's far more useful as you can dispatch on expressions, not just type.

import dispatch

@dispatch.generic()
def foo(a, b):
    """
    foo is a generic method that takes two 
    parameters and dispatches based on type
    """

@foo.when("isinstance(a, int) and isinstance(b, int)")
def foo(a, b):
    print "code for two ints"

@foo.when("isinstance(a, float) and isinstance(b, float)")
def foo(a, b):
    print "code for two floats"

@foo.when(
    # note that you can't use multi-line strings 
    # in here without backslash continuation.
    # dispatch's current parser hates them(?!)
    "(isinstance(a, str) and isinstance(b, str)) or "
    "(isinstance(a, unicode) and isinstance(b, unicode))")
def foo(a, b):
    # what makes this more interesting than dispatching on
    # basestring is that you can't mix str/unicode here.
    # also note that this is equivalent to stacking...
    print (a, b)
    print "code for two strings"
    print "or unicodes..."

8 Comments »

  1. What’s hiding behind those “import *” statements? Could you expand those?

    Comment by Guido van Rossum — 2005-03-30 @ 10:01 pm

  2. Expanded

    Comment by Bob Ippolito — 2005-03-30 @ 10:15 pm

  3. I think it’s important to point out that all of the complexity in this code comes from 1) emulating Guido’s “implicit” API where the multimethod is automatically created upon first definition, and 2) implementing exact type matching instead of using isinstance(). Without #1, most of the ‘register’ function wouldn’t be needed, and without #2, the Getattr/Pointer stuff would be unnecessary. So, with the dispatch framework it’s actually *harder* to create a crippled and implicit version of the API. :)

    Part of the reason I point this out is so that people stumbling across this page don’t end up thinking that Guido’s code is “better” than using the dispatch module, or — even worse! — they actually use this code.

    If you really want the semantics of Guido’s example and you’re using PyProtocols, you should use @foo.when("type(a) is int and type(b) is int") and not reimplement all these wheels. And if you want multiple signatures for a single method, then @foo.when("type(a) is str and type(b) is str or type(a) is unicode and type(b) is unicode") also works as intended. There’s then no need for last-method attribute hacks or stacked decorators.

    Comment by Phillip J. Eby — 2005-03-31 @ 12:55 pm

  4. Phillip, would you mind showing a more “natural” example using PyProtocols, e.g. one that requires separate initial registration and isinstance?

    Comment by Guido van Rossum — 2005-03-31 @ 1:00 pm

  5. Hi Guido. Bob has now added a spelled-out version of your example to his article; is that what you were asking for?

    Comment by Phillip J. Eby — 2005-03-31 @ 2:00 pm

  6. Yes, Phillip.

    I have to say, I just cringe when I see any kind of API that takes a string containing Pythonesque syntax a la our where() predicate. But we should probably take that offline (and I understand the constraints under which you are working.)

    Comment by Guido van Rossum — 2005-03-31 @ 5:27 pm

  7. Guido, you were the one who suggested using strings for this, in a message on Python-Dev long ago. At least, I recall you saying that strings were the right way to express Python code within Python, when somebody was asking for a block syntax. But that was a long time ago and perhaps you’ve changed your mind since. One alternative I considered was to use lambda: and getsource(), but getsource() doesn’t seem to grok lambdas.

    In any case, the @foo.when(Signature(x=int,y=int)) example I showed in the comments on your blog is functionally equivalent to the isinstance() string form, and you can do Signature(x=str,y=str) | Signature(x=unicode,y=unicode) to “or” signatures together. So, if you prefer not to use strings for when() conditions, you can construct predicates and signatures using the lower-level dispatch API. (That is, you can directly build the structure that the parser would build from a string.)

    Comment by Phillip J. Eby — 2005-03-31 @ 6:16 pm

  8. I don’t like the string forms either. The “lower-level” API Philip refers to looks reasonable to me (although I’m not clear how the x/y in Philip’s comment relate to the a/b in the parameters…)

    So far, I’ve only really watched the development of the generic function stuff, never used it, but my instinct says that I would use the “lower-level” API if at all possible, and avoid the string form whenever I could. If Python supported an “expression literal” form which was used here instead of strings, I’d be a lot happier. No idea why. Maybe it’s something as silly as the syntax highlighting my editor applies…

    Comment by Paul Moore — 2005-04-13 @ 9:29 am

RSS feed for comments on this post.

Leave a comment

WP-Hashcash: protecting you from spam.

Powered by WordPress