Python method resolution order and super
Recently, I hit into an interesting issue with python multiple inheritance.
Initially, I have a linear inheritance hierarchy A -> B -> C
class A(object):
def f(self):
print('A')
class B(A):
def f(self):
super().f()
print('B')
class C(B):
pass
Execution of C().f()
gives A B
(line break is ignored).
This works well but what if we have multiple equivalent classes of A
,
and each of them needs a corresponding C
class?
For example,
class A1(object):
def f(self):
print('A1')
class A2(object):
def f(self):
print('A2')
Execution of C1().f()
should give A1 B
, and execution of C2().f()
should give A2 B
.
One straightforward implementation is to apply the linear inheritance multiple times, i.e.,
A1 -> B1 -> C1
A2 -> B2 -> C2
where B1
and B2
have basically the same code, only differing in their base class.
This is obviously not a good design since the logic of B
is not reused.
A better solution is to use multiple inheritance, i.e.,
class C1(B0, A1):
pass
class C2(B0, A2):
pass
where B0
implements B
’s functionality and is to be defined later.
The class hierarchy looks like this
And the B0
is as follows
class B0(object):
def f(self):
super().f()
print('B')
Execution of C1.f()
indeed gives the desired result A1 B
.
Although it works, note that B0
does not inherit directly from any of the A
s.
It only knows about the A
s via the C
s!
One natural question is then
- How does
B0.f()
access the correspondingf()
of theA
s?
The core of this question is the order of function override in a complicated inheritance hierarchy, commonly known as Method Resolution Order (MRO).
According to python 3.6 doc, super
Return a proxy object that delegates method calls to a parent or sibling class of type. This is useful for accessing inherited methods that have been overridden in a class. The search order is same as that used by getattr() except that the type itself is skipped.
The mro attribute of the type lists the method resolution search order used by both getattr() and super(). The attribute is dynamic and can change whenever the inheritance hierarchy is updated.
In our case, C1.mro()
or C1.__mro__
is given by
[<class '__main__.C1'>, <class '__main__.B0'>, <class '__main__.A1'>, <class 'object'>]
Thus super().f()
from B0.f()
correctly finds the desired function A1.f()
.
There is a very nice document on the python MRO written by Dr. Michele Simionato with many examples.
One final thing to note is that new style classes (the ones inherit from object
) and classic classes use different MRO
- new style classes MRO: C3 linearization
- classic classes MRO: depth first and then left to right