Python's MRO surprises
Consider a script like this:
class A: def __init__(self): print('a') class B: def __init__(self): print('b') class C(A, B): def __init__(self): super().__init__() print('c') C()
What will it output? If you are like me, you would answer, without much thought, that it prints:
a c
To work it out you need to know about Python's MRO (Method Resolution Order): this article provides a clear explanation of how it works. In this case, we have:
>>> C.mro() [<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]
Now try this other snippet. The only change is a call to super().__init__()
in class A
.
class A: def __init__(self): super().__init__() print('a') class B: def __init__(self): print('b') class C(A, B): def __init__(self): super().__init__() print('c') C()
What will it print? Again, if you are like me you will expect to print the same. But it doesn't. It prints
b a c
I find it surprising in many ways. First of all, I am surprised that MRO is
affected by the body of __init__()
: I would have thought it is statically
determined by the class hierarchy. Secondly, I would have thought to see 'a b
c' printed, not 'b a c'. Note that if you swap the two lines of A.__init__
,
so that super()
is the last statement, it indeed prints 'a b c'.
Finally, consider this script:
class A: def __init__(self): print('a') class B: def __init__(self): super().__init__() print('b') class C(A, B): def __init__(self): super().__init__() print('c') C()
What will it print? I guessed right this time:
a c
I found a nice explanation in this blog post, so please read it for details.
The way I think about it is that MRO uses a kind-of BFS to linearise the
inheritance graph of C
: first it looks at A
, but as soon as A.__init__
is
called, it finds a call to super()
which causes to temporarily stop the
search there and visit the sibling of A
, which is B
. At this point, 'b' is
printed and execution of A.__init__
is "resumed", so 'a' is printed and
finally 'c'.
It's a complicated algorithm to fit in one's mind, I reckon. And the result
depends on the implementation of __init__
, which may not be readily
available to read: so how will one know what will happen? The recommendation I
got from various sources is: "Avoid multiple inheritance, especially in
Python". It seems like good advice.