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

A1 B0 A2 ] C1 C2

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 As. It only knows about the As via the Cs! One natural question is then

  • How does B0.f() access the corresponding f() of the As?

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