Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python method lookup rules

I find the following example mildly surprising:

>>> class Foo:
        def blah(self):
            pass


>>> f = Foo()
>>> def bar(self):
        pass

>>> Foo.bar = bar
>>> f.bar
<bound method Foo.bar of <__main__.Foo object at 0x02D18FB0>>

I expected the bound method to be associated with each particular instance, and to be placed in it at construction. It seems logical that the bound method would have to be different for each instance, so that it knows which instance to pass in to the underlying function - and, indeed:

>>> g = Foo()
>>> g.blah is f.blah
False

But my understanding of the process is clearly flawed, since I would not expect assigning a function into a class attribute would put it in instances that had already been created by then.

So, my question is two fold -

  1. Why does assigning a function into a class apply retroactively to instances? What are the actual lookup rules and processes that make this so?
  2. Is this something guaranteed by the language, or just something that happens to happen?
like image 355
lvc Avatar asked Dec 01 '22 23:12

lvc


2 Answers

You want to blow your mind, try this:

f.blah is f.blah

That's right, the instance method wrapper is different each time you access it.

In fact an instance method is a descriptor. In other words, f.blah is actually:

Foo.blah.__get__(f, type(f))

Methods are not actually stored on the instance; they are stored on the class, and a method wrapper is generated on the fly to bind the method to the instance.

like image 62
kindall Avatar answered Dec 04 '22 09:12

kindall


The instances do not "contain" the method. The lookup process happens dynamically at the time you access foo.bar. It checks to see if the instance has an attribute of that name. Since it doesn't, it looks on the class, whereupon it finds whatever attribute the class has at that time. Note that methods are not special in this regard. You'll see the same effect if you set Foo.bar = 2; after that, foo.bar will evalute to 2.

What is guaranteed by the language is that attribute lookup proceeds in this fashion: first the instance, then the class if the attribute is not found on the instance. (Lookup rules are different for special methods implicitly invoked via operator overloading, etc..)

Only if you directly assign an attribute to the instance will it mask the class attribute.

>>> foo = Foo()
>>> foo.bar
Traceback (most recent call last):
  File "<pyshell#79>", line 1, in <module>
    foo.bar
AttributeError: 'Foo' object has no attribute 'bar'
>>> foo.bar = 2
>>> Foo.bar = 88
>>> foo.bar
2

All of the above is a separate matter from bound/unbound methods. The class machinery in Python uses the descriptor protocol so that when you access foo.bar, a new bound method instance is created on the fly. That's why you're seeing different bound method instances on your different objects. But note that underlyingly these bound methods rely on the same code object, as defined by the method you wrote in the class:

>>> foo = Foo()
>>> foo2 = Foo()
>>> foo.blah.im_func.__code__ is foo2.blah.im_func.__code__
True
like image 31
BrenBarn Avatar answered Dec 04 '22 07:12

BrenBarn