So, to recap briefly, we’ve taken Guido’s five minute multimethods and made them respect scope. We’re happily creating a bunch of different implementations of foo in different contexts, some of which are multimethods and some of which are plain functions. It’s all going swimmingly. Then we try to do something like the following:
class Foo: @multimethod(Foo, int, int) def bar(self, x, y): return "Bar int and int" @multimethod(Foo, string, string) def bar(self, x, y): return "Bar string and string" def baz(self, x, y): return "Baz something and something" foo = Foo() print(foo.bar("hello", "world"))
Just as an aside, I forgot how much I hate Python’s requirement to explicitly pass self. Ugh, just look at that.
You’d hope for an output of Bar string and string. What you actually get is an error: no implementation found.
The problem here is in how Python implements methods.
Foo.bar isn’t actually method, after all: it’s a dispatcher, an object which happens to be callable. So, you get that object, and you call it with “hello” and “world”, and it sees two parameters where it’s expecting three.
So how does Python make methods work? How does it bind
bar? Well, the secret is that unlike a language like Java,
object.method(args) is not a syntactic construct in its own right. Rather, it’s an expression composed of sub-expressions, and could just as easily be written as
(object.method)(args). So, what does
(object.method) evaluate to? Normally, it would just be whatever value was assigned to the field. However, if that value is a function: then we get a special object which is callable, and when called, calls the definition of method with first
object and then
Magic! Well, arcane, anyway.
Normally, decorators will work on methods, because they return a function. We can easily make Guido’s multimethods work as methods by wrapping them in a closure which looks up and invokes the appropriate dispatcher: it’s just one level of function wrapping so we have a proper function instead of just a callable.
The problem then is that we’ve encapsulated our dispatcher, and that’s no good for our scope-respecting implementation, because we want to identify whether a function is a dispatcher or not and then do dispatchery things to it. We can’t do that to a closure. Fortunately, we don’t need to: the function wrapping we could apply is unnecessary. See, the magic above doesn’t just work on functions. It works on descriptors, of which functions are an example. Furthermore, it’s easy to turn anything into a descriptor, because descriptor is a duck type. All we need to do is:
class MethodDispatcher: def __init__(self, instance, dispatcher): self.instance = instance self.dispatcher = dispatcher def __call__(self, *args): return self.dispatcher(self.instance, *args) class Dispatcher(object): # __get__ is the descriptor duckface method we care about here def __get__(self, instance, clazz): if(instance == None): return self return MethodDispatcher(instance, self) # rest of dispatcher goes here
There is room here for a short discussion on why you might want to do this: we’re mixing two different dispatch mechanisms here. Couldn’t you replace the class method with a regular method, taking the
Foo as a first parameter?
Well, yeah, you could – but it has all sorts of interesting consequences. Having multimethods respect scope limits how you can extend them – under that brief, you can’t add to them outside the scope they were originally defined. You couldn’t define that multimethod until you knew all the classes it might have to deal with. With regular methods, however, you can add new classes without needing to address them in the pre-existing codebase. So, they fill different needs.
But that raises another question: what if I have a multimethod which doesn’t address all my cases? How can I extend them? I could have done that with Guido’s implementation!
That’s also easy: you just wrap it in another multimethod, handling the cases you want to add and passing anything else through to the original multimethod. Don’t change what already exists, create something new which contains both the original and your new requirements.