/code/blog

Code, code and code

Clojure Style Multi Methods in Python

What are multi-methods used for ? Simply put, they allow for function overloading, ie. they allow for different implementations of the same function to be provided for different contexts, and the appropriate context and therefore the implementing function to be automatically selected and performed at runtime. The typical method of function overloading is based on differing type based signatures (a carryover from the C++ / Java days). There is a multimethod module in python. That can be used for type based switching. There is another way function overloading gets used, an approach used by clojure which it terms as multi methods based on an intermediate function used to compute a value, and then switches based upon that value. Note: Switching = Coming to a fork on the road and then deciding which path to take. For an easier reading on clojure multimethods, you may want to first read Clojure multi-methods, a nice post by Alex Miller. One more reason to read that post, is that the specific situation I use to demonstrate the usage of multi methods is also identical to the one described in that post. Here’s one way to implement clojure style multimethods in python. However I will put the cart before the horse, and demonstrate how multi methods could be used in python, before explaining the underlying support functions I use to make that feasible. Situation We have a person structure with a name and an age as member attributes. We would like to print a person’s name but prefix it with Young, Adult or Senior based on the person’s age.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Declare the class
class Person(object):
    """ A person. Has a name and an age"""
    def __init__(self,name,age):
        self.name = name
        self.age = age

# Instantiate the objects
jimmy = Person("Jimmy",5)
alex = Person("Alex",36)
edna = Person("Edna",99)

# Run the multi method
print name(jimmy)
print name(alex)
print name(edna)

#Expected output is 
#        Young Jimmy
#        Adult Alex
#        Senior Edna
To do the same, we will actually declare a switcher function which computes a value to switch upon.
1
2
3
4
5
def ticket(person):
    """ The switcher function to decide the result to be used for multi method switching """
    if person.age < 16 : return "young"
    elif person.age >= 66 : return "senior"
    else : return "adult"
We shall need a switcher which will switch based on this value.
1
2
# Declare the existence of a multi method switcher
name = multi(ticket)
Ahh, so we now need three methods to be declared, one each for each value. These we shall do so using decorator @multi_method. Note all the three functions have the same name. Both the decorator and the support that allows the functions to have the same name comes up later.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Add the first switched function for result value "young"
@multi_method(name, "young")
def name(person):
    return "Young " + person.name

# Add the next switched function for the result value "adult"
@multi_method(name, "adult")
def name(person):
    return "Adult " + person.name

# Add yet another switched function for the result value "senior"
@multi_method(name, "senior")
def name(person):
    return "Senior " + person.name
Now this can be made to work. But it needs a couple of constructs behind the scenes. First we need the constructor for a multi function, which accepts the switcher function (in this case ticket).
1
2
3
4
5
6
7
8
9
10
11
12
13
def multi(switcher_func):
    """ Declares a multi map based method which will switch to the
    appropriate function based on the results of the switcher func"""

    def dispatcher(*args, **kwargs):
        key = switcher_func(*args, **kwargs)
        func = dispatcher.dispatch_map[key]
        if func :
            return func(*args,**kwargs)
        else :
            raise Exception("No function defined for dispatch key: %s" % key)
    dispatcher.dispatch_map = {}
    return dispatcher
This declares an inner function dispatcher, which has the logic to perform the switch based on the supplied switcher function and the run time arguments to the function. What this method however does is that it sets up a dictionary called dispatch_map as an attribute of the dispatcher function which will be used to store the possible result values and the resultant function to switch to. Having done that, we still need to define each of the individual functions for each possible value of the switcher function. That is done using the following decorator where the dispatch_map that got defined earlier is populated with the appropriate result and the function to dispatch to.. Note that the function name gets changed in order to avoid a conflict.
1
2
3
4
5
6
7
8
9
10
def multi_method(dispatcher, result):
    """ The multi method decorator which allows one method at a time
    to be added to the broader switch for the given result value"""
    def inner(wrapped):
        dispatcher.dispatch_map[result] = wrapped
        # Change the method name from clashing with the multi and allowing
        # all multi methods to be written using the same name
        wrapped.func_name = "_" + wrapped.func_name + "_" + str(result)
        return dispatcher
    return inner
There’s a way to use these capabilities for class members. So if I wanted to dispatch the __str__ method based on the values, the class would now be
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person(object):
    """ A person. Has a name and an age"""
    def __init__(self,name,age):
        self.name = name
        self.age = age

    __str__ = multi(ticket)

    @multi_method(__str__,"young")
    def str_young(self):
        return "Young " + self.name

    @multi_method(__str__,"adult")
    def str_adult(self):
        return "Adult " + self.name

    @multi_method(__str__,"senior")
    def str_senior(self):
        return "Senior " + self.name
Not bad for an hours work. Update 1: Even as I had just tweeted this codeblog post, another tweet caught my attention (possibly unrelated) which expected the switching condition for each function to be defined at each function level and not as a composite function returning some value. Since this was fresh in my mind, I thought that would be an interesting option too, and in a few minutes had another implementation based on the conditions being specified at a per function level. Here’s the separate version for the same. Pay attention to the fact that there is no ticket function, and that condition for each function is declared in the decorator itself.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def multi():
    """ Declares a multi map based method which will switch to the
    appropriate function based on the results of the switcher func"""

    def dispatcher(*args, **kwargs):
        for condition, func in dispatcher.functions :
            if condition(*args, **kwargs) :
                return func(*args,**kwargs)
        raise "No function identified for dispatch key"

    dispatcher.functions = []
    return dispatcher

def multi_method(dispatcher, condition):
    """ The multi method decorator which allows one method at a time
    to be added to the broader switch for the given result value"""
    def inner(wrapped):
        dispatcher.functions.append((condition, wrapped))
        # Change the method name from clashing with the multi and allowing
        # all multi methods to be written using the same name
        wrapped.func_name = "_" + wrapped.func_name + "_" + str(condition)
        return dispatcher
    return inner

class Person(object):
    """ A person. Has a name and an age"""
    def __init__(self,name,age):
        self.name = name
        self.age = age

# Declare the existence of a multi method switcher
name = multi()

# Add the first switched function for result value "young"
@multi_method(name, lambda p : p.age < 16)
def name(person):
    return "Young " + person.name

# Add the next switched function for the result value "adult"
@multi_method(name, lambda p : 16 <= p.age < 66)
def name(person):
    return "Adult " + person.name

# Add yet another switched function for the result value "senior"
@multi_method(name, lambda p : 66 <= p.age)
def name(person):
    return "Senior " + person.name

# Instantiate the objects
jimmy = Person("Jimmy",5)
alex = Person("Alex",36)
edna = Person("Edna",99)

# Run the multi method
print name(jimmy)
print name(alex)
print name(edna)
Update 2 I was requested to demonstrate this in terms of the clojure polymorphism as described in Clojure - runtime_polymorphism (code snippet from that page listed below) :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(defmulti encounter (fn [x y] [(:Species x) (:Species y)]))
(defmethod encounter [:Bunny :Lion] [b l] :run-away)
(defmethod encounter [:Lion :Bunny] [l b] :eat)
(defmethod encounter [:Lion :Lion] [l1 l2] :fight)
(defmethod encounter [:Bunny :Bunny] [b1 b2] :mate)
(def b1 {:Species :Bunny :other :stuff})
(def b2 {:Species :Bunny :other :stuff})
(def l1 {:Species :Lion :other :stuff})
(def l2 {:Species :Lion :other :stuff})
(encounter b1 b2)
-> :mate
(encounter b1 l1)
-> :run-away
(encounter l1 b1)
-> :eat
(encounter l1 l2)
-> :fight
Here’s how the same effect can be had in python using the first version of multi and multi_method I described above.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Declare the existence of a multi method switcher
encounter = multi(lambda x,y : (x["Species"], y["Species"]))

@multi_method(encounter, ("Bunny","Lion"))
def encounter(a1, a2):
    return "run-away"

@multi_method(encounter, ("Lion","Bunny"))
def encounter(a1, a2):
    return "eat"

@multi_method(encounter, ("Bunny","Bunny"))
def encounter(a1, a2):
    return "mate"

@multi_method(encounter, ("Lion","Lion"))
def encounter(a1, a2):
    return "fight"

b1 = {"Species" : "Bunny", "Other" : "Stuff"}
b2 = {"Species" : "Bunny", "Other" : "Stuff"}
l1 = {"Species" : "Lion", "Other" : "Stuff"}
l2 = {"Species" : "Lion", "Other" : "Stuff"}

print encounter(b1, b2)
print encounter(b1, l1)
print encounter(l1, b1)
print encounter(l1, l2)
The resultant output is
1
2
3
4
mate
run-away
eat
fight

Comments