Tuesday, September 15, 2015

A Tale of Two Methods

Python has some interesting aspects related to functional programming. Its classes and class instances both have methods that can be assigned to other variables and passed around and they can have their methods replaced (a technique, called monkey patching, that usually leads to pain and suffering).

One issue with assigning a method from a class or instance to a variable that can cause significant confusion is that the behavior is different depending on whether the method's being taken from the class or from an instance of the class. Once you know what's going on, the behavior's fairly intuitive, but until then it can be really confusing.

For the sake of discussion, let's define a class, Foo.

class Foo:
    def __init__(self, name):
        self.name=name
        self.aka = "who knows?"

    def bar(self):
        print("Greetings, from {0} (aka {1})".format(self.name, self.aka))

    def set_aka(self, aka):
        self.aka = aka

Then we can instantiate a Foo:
foo1 = Foo("Batman")

Now that we've got Foo and an instance of Foo, let's take their methods and see what we get!

foos_bar = Foo.bar
foo_1s_bar = foo1.bar

foos_bar: <unbound method Foo.bar>
foo_1s_bar: <bound method Foo.bar of <__main__.Foo instance at 0x7f8ae4028b48>>

I've highlighted an important distinction between the two; the method taken from Foo, is an unbound method, whereas the one from foo1 is a bound method. So, what exactly does that mean? Well, let's try calling the methods and see!

foos_bar()

TypeError: unbound method bar() must be called with Foo instance as first argument (got nothing instead)

So that's what that unbound meant; it has no "self" tied to it, so there's no argument being passed for self. Given that that's an unbound method, but foo_1s_bar was a bound method, it seems likely that its behavior will differ. Let's see!

foo_1s_bar()

Greetings, from Batman (aka who knows?)

Ok, that worked, but why? I can't claim to know the details of Python's implementation, however the behavior suggests that the call to Foo is creating an object and using currying to create a method for each of the class' methods that have a
self
bound into them; i.e. foo1's bar is a method that takes no arguments, because it has already had foo1 bound to it as self. We can test that by instantiating another Foo and setting its bar method.

foo2 = Foo("Joker")
foo2.bar = foo_1s_bar
foo2.bar()

Greetings, from Batman (aka who knows?)

Aha! foo_1s_bar retains its self, even if you attach it to a different instance of Foo! Now that we've seen what the bound method is all about, what happens if we pass an instance of Foo to foos_bar?

foos_bar(foo1)
foos_bar(foo2)

Greetings, from Batman (aka who knows?)
Greetings, from Joker (aka who knows?)


We've mostly covered the difference between bound and unbound methods, but there's one more oddball case. What if we attach foo1's bar to Foo and then instantiate another Foo?

Foo.bar = foo_1s_bar
foo3 = Foo("The Shadow")
foo3.bar()

Greetings, from Batman (aka who knows?)

Out of curiosity, I also tested calling bar on a Foo that was instantiated before replacing Foo's bar (foo2; the one set to be Joker). Reasonably, you might expect that it'd call that foo's bar, totally ignoring the change to Foo. If it was doing currying to create the methods, then it definitely wouldn't have any effect. Alas...

Greetings, from Batman (aka who knows?)

Well, that rules out currying at instantiation as the implementation of bound methods, and gives us a major warning about the dangers of replacing unbound methods!

There are two more things to test: setting a bound method from one instance onto another instance and setting a bound method onto a class as a method that wasn't defined. The results of the latter aren't exactly a shocker now that we've seen what happens when you replace an unbound method with a bound one. It has the exact same behavior. The former, on the other hand, could go either way. Unfortunately, it does not. Its self is still bound to the object from which the method was referenced.