Python Method Resolution Order
Python supports multiple inheritance. That is, a child class can inherit from more than one parent. But, what happens if a method is implemented in more than one ancestor? Which of the methods available will be called?
If you already have a pretty good grasp on Python’s MRO, test yourself with the advanced examples.
A basic example
If you have crossed paths with multiple inheritance in the past, you may know that if you have a child class with two parent classes, and make a call in the child class to a method that is implemented in both parents, Python’s Method Resolution Order (MRO) will look for it in the parents, from left to right as specified in the child class definition. And, in particular, only the first implementation found following this order will be executed.
Such is the case of the following snippet of code:
class PrivateCar:
def driven_for(self):
print("Driven for personal purposes.")
class Taxi:
def driven_for(self):
print("Driven for business purposes.")
class RentalCar(PrivateCar, Taxi):
def rental_car_driven_for(self):
self.driven_for()
print("A rental car company owns the car.")
if __name__ == "__main__":
rental_car = RentalCar()
rental_car.rental_car_driven_for()
If we execute this snippet of code, “Driven for personal purposes. A rental car company owns the car.”
will be printed, as the MRO sees that driven_for
is not implemented in RentalCar
, it looks for it in PrivateCar
,
finds it, and uses it. If the method hadn’t been implemented in PrivateCar
, then it would have
looked for it in Taxi
. Easy peasy.
Not so quick, my friend
Let’s extend slightly the code snippet above and introduce a Vehicle
base class and, since normally
cars are driven for personal purposes, we will move the implementation of driven_for
from
PrivateCar
to this Vehicle
class. To still have the same behaviour, PrivateCar
will now
inherit from Vehicle
.
Additionally, let’s add an argument to the driven_for
method from Taxi
to include the color of
the taxi. What do you expect this code to print?
class Vehicle:
def driven_for(self):
print("Driven for personal purposes.")
class PrivateCar(Vehicle):
pass
class Taxi:
def driven_for(self, color):
print("Driven for business purposes")
print(f"They are normally {color}.")
class RentalCar(PrivateCar, Taxi):
def rental_car_driven_for(self):
self.driven_for()
print("A rental car company owns the car.")
if __name__ == "__main__":
rental_car = RentalCar()
rental_car.rental_car_driven_for()
What your intution may tell you
Following from the base example, you may expect left-to-right MRO. That is, the method driven_for
is not implemented in RentalCar
, so we check in PrivateCar
, who does not implement it, so we
check in its parent, Vehicle
, find it and use it. Therefore, this snippet of code should still print:
“Driven for personal purposes. A rental car company owns the car.”.
Executing the code will show you exactly that and, therefore, you may be lead to think that the MRO follows a depth-first search approach, which, unfortunately, is not true.
A counter-example to depth-first search (The Diamond Inheritance problem)
If we are honest to ourselves, Taxi
s are indeed vehicles, so they should also inherit from the Vehicle
class, so we will add a connection between the two. That is, our code will look as shown below and
we will not expect any changes in behaviour in our code.
class Vehicle:
def driven_for(self):
print("Driven for personal purposes.")
class PrivateCar(Vehicle):
pass
class Taxi(Vehicle):
def driven_for(self, color):
print("Driven for business purposes")
print(f"They are normally {color}.")
class RentalCar(PrivateCar, Taxi):
def rental_car_driven_for(self):
self.driven_for()
print("A rental car company owns the car.")
if __name__ == "__main__":
rental_car = RentalCar()
rental_car.rental_car_driven_for()
What see when you execute the code
Traceback (most recent call last):
File "/home/hector/diamond_example.py",
line 19, in <module>
rental_car.rental_car_driven_for()
File "/home/hector/diamond_example.py",
line 14, in rental_car_driven_for
self.driven_for()
TypeError: Taxi.driven_for() missing 1
required positional argument: 'color'
What!? I thought MRO was depth-first search. Why does it then visit the Taxi
method instead
of behaving as before!? This looks like breadth-first search! But I thought… :(
The explanation
MRO does neither follow breadth-first nor depth-first ordering. Instead, it does something different. It is not so easy to explain with words, so let’s try to understand the examples here.
Python’s MRO will not visit the grandparent class (Vehicle
in this case) until all its children
have been checked. That is, in the first example, we follow the order RentalCar -> PrivateCar -> Vehicle
because PrivateCar
is the only child of Vehicle
. However, in the second example, the MRO follows
RentalCar -> PrivateCar -> Taxi -> Vehicle
, because it needs to visit both PrivateCar
and Taxi
before visiting Car
.
In other words, after visiting a parent, we will need to ask ourselves whether we have visited
all the children of any of its parents. If so, we will visit it. If not, we will continue with the
next parent. The children considered, however, will be only those in the ascendent path from the
root class. That is, if the Vehicle
class also had a Bycicle
child (which our RentalCar
does
not inherit from), this class would not be visited.
A formal definition of the algorithm followed by Python’s MRO can be found
here. However, if you find this algorithm
too confusing or simply do not want to spend time reading it, you can always call a class’ method
mro
and that will return the order of resolution. For example, if print what RentalCar.mro()
returns in the last example, we will get:
[<class '__main__.RentalCar'>, <class '__main__.PrivateCar'>,
<class '__main__.Taxi'>, <class '__main__.Vehicle'>,
<class 'object'>]
Note that every class without any explicit parent, always inherits of the base class ‘object’.
Now, if you think you’re ready, test your understanding in the advanced examples that I have added below. Good luck!
Advanced examples
Example #1
What MRO will be followed in this case?
Solution: E -> B -> C -> D -> A
Explanation: If you have understood the basic case, adding another parent class should not make it any more complex. Following the rule of visiting all children before visiting the parent, tells us to visit B, C and D before finally visiting A.
Example #2
What MRO will be followed in this case?
Solution: F -> C -> D -> A -> E -> B
Explanation: Having parents with different grandparents makes it more complex. First, we start visiting the parents with C. Then we ask ourselves, have we visited all the children of C’s parent (A)? No, we haven’t. So we continue with the parents of F, and visit D. We ask ourselves the same question again. In this case, all of A’s children have been visited so we visit A. Then, we continue with F’s parents, E, and then finally B.
Example #3
What MRO will be followed in this case?
Solution: F -> D -> A -> E -> B -> C
Explanation: We start visiting F’s parents with D. Then we ask ourselves, have we visited all children of a parent of D? In this case, we have visited all children of A, so we can visit it. However, we can’t visit B yet, as we have not visited E (one of its children yet). Then we continue with the parents from F, and visit E. Have all the children of one of E’s parents been visited? Yes, of B, so we can visit it. Finally, we visit C.
Example #4
What will this snippet print?
class A:
def __init__(self):
print("I'm A's method")
class B(A):
def __init__(self):
print("I'm B's method")
super().__init__()
class C(A):
def __init__(self):
print("I'm C's method")
super().__init__()
class D(B, C):
def __init__(self):
print("I'm D's method")
super().__init__()
if __name__ == "__main__":
D()
Solution:
I'm D's method
I'm B's method
I'm C's method
I'm A's method
Explanation: super() calls also follow the MRO!! This means that, we are calling C’s __init__
method from B even though the classes are only connected through children and ancestors!!!!
That is, the super() call in D finds B’s constructor. The super() call in B, finds the constructor of the next class in the MRO, that is, C. Finally, the super() call in C, finds the constructor of A.
Oh Python, so intuitive and easy to learn :eyeroll:
Example #6
A more explicit and preferable version if calling ancestors’ constructors:
class A:
def __init__(self):
print("I'm A's method")
class B(A):
def __init__(self):
print("I'm B's method")
A.__init__(self)
class C(A):
def __init__(self):
print("I'm C's method")
A.__init__(self)
class D(B, C):
def __init__(self):
print("I'm D's method")
B.__init__(self)
if __name__ == "__main__":
D()
Solution:
I'm D's method
I'm B's method
I'm A's method
Explanation: This alternative way of calling the ancestors’ constructors directly uses the class reference available to call the method. In my opinion, this is a nicer, cleaner, more explicit version to deal with multiple inheritance constructor calls.
Takeaways
- Multiple inheritence in Python can be a nightmare.
- Python’s MRO does NOT follow breadth-first or depth-first ordering.
- Use the class method
mro
to quickly understand complex inheritance trees. - super() calls follow the static class order defined by Python’s MRO!
- Consider using
Class.__init__
for more explicit parent constructor calls.