Python MRO and Mixin Classes

In practice we don’t usually think about how Python’s MRO works. Complex multiple inheritance legitemately considered to be a bad thing. Usually we have very straight forward hierarchies like this:

In [1]: class A:
   ...:     def a(self):
   ...:         return 42
   ...:
   ...:
   ...: class B(A):
   ...:     def b(self):
   ...:         return 43
   ...:

In [2]: b = B()
In [3]: b.b()
Out[3]: 43

In [4]: b.a()
Out[4]: 42

But one application for relatively complex inheritance hierarchies is mixin classes. We can use the following so called diamond inheritance to add some checks to the method do we are interested in.

class Top:
    def __init__(self):
        print('Top class')

    def do(self, value):
        print('Top.do')


class Left(Top):
    def __init__(self):
        print('Left class')
        super().__init__()

    def do(self, value):
        print('Left.do')
        assert isinstance(value, int), 'Left.do expected integer values only'
        # We explicitly return parent's do method.
        # If there was no such return, then Right.do would never be called
        # when Bottom.do() is called
        return super().do(value)


class Right(Top):
    def __init__(self):
        print('Right class')
        super().__init__()

    def do(self, value):
        print('Right.do')
        assert value >= 0, 'Right.do expected positive numbers only'
        return super().do(value)


class Bottom(Left, Right):
    def __init__(self):
        print('Bottom class')
        super().__init__()

    def do(self, value):
        print('Bottom.do')
        return super().do(value)

We can instantiate Top class when no checks in do method needed, we can use class Left when workgin with integers or we can use Right class when working with non-negative numbers. When both checks are needed though, we use the Bottom class.

The initialization of the Bottom class reveals it’s MRO:

In [2]: b = Bottom()
Bottom class
Left class
Right class
Top class

In [3]: Bottom.mro()
Out[3]: [__main__.Bottom, __main__.Left, __main__.Right, __main__.Top, object]

Now let’s see how multiple inheritance works:

In [4]: b.do(10)
Bottom.do
Left.do
Right.do
Top.do

In [5]: b.do(10.5)
Bottom.do
Left.do
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-5-1eceeaaa850f> in <module>
----> 1 b.do(10.5)

<ipython-input-1-f358c005e4dd> in do(self, value)
     39     def do(self, value):
     40         print('Bottom.do')
---> 41         return super().do(value)
     42

<ipython-input-1-f358c005e4dd> in do(self, value)
     14     def do(self, value):
     15         print('Left.do')
---> 16         assert isinstance(value, int), 'Left.do expected integer values only'
     17         # We explicitly return parent's do method.
     18         # If there was no such return, then Right.do would never be called

AssertionError: Left.do expected integer values only

In [6]: b.do(-10)
Bottom.do
Left.do
Right.do
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-6-f82b823fc4ec> in <module>
----> 1 b.do(-10)

<ipython-input-1-f358c005e4dd> in do(self, value)
     39     def do(self, value):
     40         print('Bottom.do')
---> 41         return super().do(value)
     42

<ipython-input-1-f358c005e4dd> in do(self, value)
     18         # If there was no such return, then Right.do would never be called
     19         # when Bottom.do() is called
---> 20         return super().do(value)
     21
     22

<ipython-input-1-f358c005e4dd> in do(self, value)
     28     def do(self, value):
     29         print('Right.do')
---> 30         assert value >= 0, 'Right.do expected positive numbers only'
     31         return super().do(value)
     32

AssertionError: Right.do expected positive numbers only

See more about the inner workings of the MRO in my article about C3 linearisation algorithm.