Skip to content
Advertisement

How is __mro__ different from other double underscore names?

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.

Advertisement