I stumbled upon this behavior for double underscore name that I don’t understand:
class A: pass class B: pass class C(A,B): __id__ = 'c' c = C() print(C.__mro__) # print the method resolution order of class C #print(c.__mro__) # AttributeError: 'C' object has no attribute '__mro__' print(C.__id__) # print 'c' print(c.__id__) # print 'c'
I know about the name mangling for __name
, which doesn’t apply for __name__
(more for overloading operator methods). __id__
behaves just like a regular class variable which can be accessed via Class name as well as instance.
However, __mro__
can only be accessed via Class name and in fact I can even explicitly introduce __mro__
in C:
class C(A,B): __mro__ = 'bla' print(C.__mro__) # print the method resolution order of class C print(c.__mro__) # print 'bla'
I’d like to understand if this behavior is some python internal magic or can it be achieved in regular python code.
[python version 3.4.3]
Advertisement
Answer
This has to do with the lookup order.
Letting descriptors aside, python first checks the objects __dict__
to find an attribute. If it cannot find it, it will look at the class of the object and the bases of the class to find the attribute. If it cannot be found there either, AttributeError is raised.
This is probably not understandable, so let us show this with a short example:
#!/usr/bin/python3 class Foo(type): X = 10 class Bar(metaclass=Foo): Y = 20 baz = Bar() print("X on Foo", hasattr(Foo, "X")) print("X on Bar", hasattr(Bar, "X")) print("X on baz", hasattr(baz, "X")) print("Y on Foo", hasattr(Foo, "Y")) print("Y on Bar", hasattr(Bar, "Y")) print("Y on baz", hasattr(baz, "Y"))
The output is:
X on Foo True X on Bar True X on baz False Y on Foo False Y on Bar True Y on baz True
As you can see, X
has been declared on the metaclass Foo
. It is accessible through the instance of the metaclass, the class Bar
, but not on the instance baz
of Bar
, because it is only in the __dict__
in Foo
, not in the __dict__
of Bar
or baz
. Python only checks one step up in the “meta” hierarchy.
For more on metaclass magic, see the excellent answers on the question What is a metaclass in python?.
This, however, is not sufficient to describe the behaviour, because __mro__
is different for each instance of Foo
(that is, for each class).
This can be achieved using descriptors. Before the attribute name is looked up at the objects __dict__
, python checks the __dict__
of the class and its bases to see if there is a descriptor object assigned to the name. A descriptor is any object which has a __get__
method. If that is the case, the descriptor objects __get__
method is called and the result is returned from the attribute lookup. With a descriptor assigned to an attribute of the metaclass, the behaviour seen can be achieved: The descriptor can return a different value based on the instance argument, but nevertheless the attribute can only be accessed through the class and the metaclass, not instances of the class.
A prime example of descriptors is property
. Here is a simple example with a descriptor which has the same behaviour as __mro__
:
class Descriptor: def __get__(self, instance, owner): return "some value based on {}".format(instance) class OtherFoo(type): Z = Descriptor() class OtherBar(metaclass=OtherFoo): pass other_baz = OtherBar() print("Z on OtherFoo", hasattr(OtherFoo, "Z")) print("Z on OtherBar", hasattr(OtherBar, "Z")) print("Z on other_baz", hasattr(other_baz, "Z")) print("value of Z on OtherFoo", OtherFoo.Z) print("value of Z on OtherBar", OtherBar.Z)
The output is:
Z on OtherFoo True Z on OtherBar True Z on other_baz False value of Z on OtherFoo some value based on None value of Z on OtherBar some value based on <class '__main__.OtherBar'>
As you can see, OtherBar
and OtherFoo
both have the Z
attribute accessible, but other_baz
does not. Still, Z
can have a different value for each OtherFoo
instance, that is, each class using the OtherFoo
metaclass.
Metaclasses are confusing at first, and even more so when descriptors are in play. I suggest reading up on metaclasses the linked question, as well as descriptors in python in general.