Dig into Python super() and MRO
By reading this post, I assume you have already understood the meaning of super() and “MRO” in Python. Here is a short summary. MRO stands for Method Resolution Order and is used to determine how a method is looked up in class inheritance. The C3 algorithm describes how to build a linearization of a class hierarchy, which is an ordered list of the ancestors, i.e., SubClass.__mro__.
If super() is not used, a method call of a subclass will follow the MRO to find the nearest parent or sibling who has the given method. Once found, it stops.
However, if super() is used, it is a little more complex regarding the execution order. Below is an example.
class A(object):
def go(self):
print("A")
class B(A):
def go(self):
print("B1")
super(B, self).go()
print("B2")
class C(A):
def go(self):
print("C1")
super(C, self).go()
print("C2")
class D(B,C):
def go(self):
print("D1")
super(D, self).go()
print("D2")
D().go()
# output
# D1
# B1
# C1
# A
# C2
# B2
# D2
It first executes from D.go() to B.go() then to C.go() and eventually A.go().
At first glance, I was very confused about why the super(B, self).go() in B.go() is calling C.go() instead of A.go(), given that A is B’s parent. I realized that it follows the MRO of class D (D.__mro__ as below), but I am still wondering what the magic is that the super in B knows that the next class is C instead of A.
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <type 'object'>)
After digging into the CPython source code, I found a clear explanation here. Quoted below:
The object returned by
super()also has a custom:meth:__getattribute__method for invoking descriptors. The callsuper(B, obj).m()searchesobj.__class__.__mro__for the base class A immediately following B and then returnsA.__dict__['m'].__get__(obj, B). If not a descriptor, m is returned unchanged. If not in the dictionary, m reverts to a search using:meth:object.__getattribute__.
The implementation details are in
:c:func:super_getattro()in:source:Objects/typeobject.c. and a pure Python equivalent can be found in Guido’s Tutorial.
Let’s re-consider the example above according to the quoted explanation.
First, the self in super(B, self).go() is a class instance of D. Then the call super(B, self).go() will search self.__class__.__mro__–in our case, D.__mro__–for the class immediately following B, which is C. Then returns C.__dict__['go'].__get__(self, B). Essentially, it calls C.go().
Now the magic of super() and MRO is clear. Thank you for reading.