Thursday, January 19, 2012

Explaining bound and unbound methods in Python by examples

Python 2.7.6 (default, Feb 26 2014, 13:22:43) 
>>> class A():  # a class for tests
...     def f(self):
...         print 'Original implementation'
>>> a = A()  # create an class instance
>>> a.f()  # call instance method
Original implementation
>>> a.f  # instance method is bound to that instance
<bound method A.f of <__main__.A instance at 0x7f6757a4e488>>
>>> A.f  # class method is a function is is not bound to an instance, but knows the class it belongs to
<unbound method A.f>
>>> A.f()  # doesn't work - the method is unbound - it doesn't know to which instance it belongs to
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unbound method f() must be called with A instance as first argument (got nothing instead)
>>> A.f(a)  # but we can pass the instance manually
Original implementation
>>> A.f(1)  # unbound method is checking that it was passed an instance of the class the method is bound to
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unbound method f() must be called with A instance as first argument (got int instance instead)
>>> def f2(self):
...     print 'Another implementation'
>>> A.f = f2  # replace implementation at the class level
>>> a.f()  # check if the method was replaced
Another implementation
>>> def f3(self):
...     print 'Third implementation'
>>> a.f = f3  # try to replace an instance method, so other instances would have old implementation
>>> a.f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f3() takes exactly 1 argument (0 given)
>>> a.f
<function f3 at 0x7f6753ff10c8>
>>> a.f is f3
>>> f3.__get__(a, A)  # All functions are also descriptors, so you can bind them by calling their __get__ method:
<bound method A.f3 of <__main__.A instance at 0x7f6757a4e488>>
>>> a.f = f3.__get__(a, A)
>>> a.f()  # `a` instance has the new implementation
Third implementation
>>> A.f(a)
Another implementation
>>> A().f()  # and other instances have older implementation
Another implementation

In Python 3 unbound methods were removed:
the "unbound method" type has entirely disappeared -- a method, until and unless it's bound, is just a function, without the weird "type-checking" unbound methods used to perform. 

Example in Python 3

Python 3.2.3 (default, Oct 19 2012, 19:53:16) 
>>> class A():
...     def func(self):
...         print('A.func called')
>>> def func2(self):
...     print('func2 called')
>>> A.func
<function func at 0x7f58342fdc00>
>>> A.__dict__['func']
<function func at 0x7f58342fdc00>
>>> A.__dict__['func'] is A.func  # the same object? (functions are objects)
>>> a = A()  # create an instance of class A
>>> a.func is A.func  # looks like these are different things!
>>> a.func  # try to get the `func` method
<bound method A.func of <__main__.A object at 0x7f58342f2b10>>
>>> a.func is a.func  # even stranger!
>>> f1 = a.func
>>> f2 = a.func
>>> f1, f2
(<bound method A.func of <__main__.A object at 0x7f58342f2b10>>, <bound method A.func of <__main__.A object at 0x7f58342f2b10>>)
>>> id(f1), id(f2)  # these are clearly two different objects

Some explanations on descriptors

How could that be that `a.f is not a.f`?

Each time you try to access a method using a dot (`a.func`) Python will look into `__dict__` dictionary of object `a`  for an attribute name `func` [1]. If it's not found there (in case of a method it's usually not there), Python will look into class's `__dict__` for the attribute `func`. `def` statement inside a class definition puts a function into class's `__dict__`, so it's there. So Python found the attribute `func` of function type. Then Python will see if the attribute is a descriptor. All functions on Python are objects implementing descriptor protocol - they are non-data descriptors, having `__get__` method. So when you access `a.func` Python will not return `func` at is is, but will return `A.__dict__['func'].__get__(a, A)` according to descriptor protocol. This means that `A.func is not a.func`, because `a.func` is a wrapper around `A.func`, and that `a.func is not a.func`, because each access to the method returns a new wrapper.

[1] This is true for methods, they being non-data descriptors:
Data and non-data descriptors differ in how overrides are calculated with respect to entries in an instance’s dictionary. If an instance’s dictionary has an entry with the same name as a data descriptor, the data descriptor takes precedence. If an instance’s dictionary has an entry with the same name as a non-data descriptor, the dictionary entry takes precedence.
More info here:

Monkey-patching methods

The method access thing implies more issues. It means that if you want to replace a method with another implementation on runtime (aka "monkey-patching"), you can do it in two ways. You can assign `A.func` another function and this will call your new `func` implementation in all existing and new instances of class `A` if you are access it via dot (`self.func()` or `obj.func()`, i.e. you haven't stored reference to the old implementation somewhere) or you haven't monkey-patched method only of that particular instance.
>>> A.func.__get__ # every function is a descriptor, having `__get__` method
<method-wrapper '__get__' of function object at 0x7f58342fdc00>
>>> func2.__get__
<method-wrapper '__get__' of function object at 0x7f58342f3af0>
>>> a2 = A()
>>> a.func()
A.func called
>>> a2.func()
A.func called
>>> a2.func = func2.__get__(a2, A) # replace method of a particular instance
>>> a2.func() # new implementation
func2 called
>>> a.func() # other instance have default old implementation
A.func called

>>> def func3(self):
...     print('func3 called')
>>> A.func = func3 # replace default implementation
>>> a.func() # new implementation
func3 called
>>> a2.func() # but this instance was patched separately - has its own implementation
func2 called
The code was highlighted using pygments

No comments:

Post a Comment