Today we’re going to look at the Python Method Resolution Order or MRO for short. If you’ve been following the tutorials on Python classes and inheritance, and you’ve been practicing in code, you’ll understand that once the hierarchy of classes moves into multiple inheritances, you may return strange results or end up with incomprehensible errors. But, on the other hand, if you understand Python’s search order as it climbs the family tree, your coding life gets easier.
Read these articles if you need a refresher on Python classes and inheritance. Feel free to watch this video as you skim over the article:
What is the Python Method Resolution Order(MRO)?
When we call an attribute or method in a class with multiple inheritances, Python follows a specific order when searching for the item we seek. That order is called the method resolution order and it complies with two rules:
- Children will precede their parents in the sort process
- When a child class inherits from multiple parent and grandparent classes, the search order follows the order specified in the
__bases__
attribute.
To understand these rules, we have three concepts to learn. The __bases__
attribute, the __mro__
attribute, and the Python built-in super class ‘object’.
One caveat here, there are two methods of MRO depending on the version of Python you are using. While the difference between them is unlikely to impact most of us in everyday use, I will assume that if you are learning Python today, you are using a 3.x version, and for that reason, I will speak to the new style of classes in this article.
What is the __bases__ attribute?
The __bases__
attribute is a Python built-in class attribute that will output the base classes of any class you call it on.
Let’s call the __bases__
attribute on an example of multiple inheritances to see the output. Here’s a block diagram of what the code will display. Two Grandparent classes, flowing to a Parent class, which then passes to a Child class.
And the code is here. The stock and warehouse classes are Grandparents, the produce class is the Parent, and fruit is the Child.
class stock(): # Grandparent pass class warehouse(): # Grandparent pass class produce(stock, warehouse): # Parent pass class fruit(produce): # Child pass print(fruit.__bases__) print(produce.__bases__) # Result (<class '__main__.produce'>,) (<class '__main__.stock'>, <class '__main__.warehouse'>)
Note that the __bases__
call on the fruit shows produce as the Parent
. Calling __bases__
on the produce
class returns stock
and warehouse
as its Parent
. As stated in the two rules earlier, stock
and warehouse
is therefore the search order that Python will follow when searching the tree.
Introducing the __mro__ attribute
This read-only attribute in Python, when called, returns a tuple of the classes that are considered by Python when searching for the base classes during method resolution. Let’s call the __mro__
on the stock
class we used in the last example to see what returns.
class stock(): pass print(stock.__mro__) # Result (<class '__main__.stock'>, <class 'object'>)
Note that there are two items referenced in the tuple. The first is the stock
class, as you would expect, as that is the current class. But what is the object class that is mentioned? The stock
class is a parent class and doesn’t have any parents of its own, does it?
Python base object class
Python has a built-in super class called object
, which is the parent class for all new classes that don’t explicitly inherit from elsewhere. For example, our stock class in the preceding code didn’t have any inheritance specified within the parentheses, so it inherits from Python’s built-in super class, called object
. So when we ask Python to print the Method Resolution Order or MRO for any class, you’ll see the final class returned will be object
.
Using __mro__ to see the order of method resolution
If we return to our complete code, we’ll call the __mro__
attribute on the child class, fruit
, and see what is returned.
class stock(): # Grandparent pass class warehouse(): # Grandparent pass class produce(stock, warehouse): # Parent pass class fruit(produce): # Child pass print(fruit.__mro__) # Result (<class '__main__.fruit'>, <class '__main__.produce'>, <class '__main__.stock'>, <class '__main__.warehouse'>, <class 'object'>)
As you’d now expect from our previous discussion, we return the method resolution order for our hybrid inheritance structure. The return tuple follows the order of the two __bases__
returns we received earlier; it shows the order as starting with the current class fruit
, then produce
, stock
, warehouse
, and finally the super class ‘object
’.
More complex structures
Our examples so far have been elementary and unproblematic, but what if we have a slightly more complex structure? Here’s a block diagram to show an example to help illustrate how Python works behind the scenes.
Then here is the code represented by that block diagram.
class stock(): # Grandparent pass class warehouse(stock): # Parent 1 pass class supplier(stock): # Parent 2 pass class canned(warehouse, supplier): # Child pass print(canned.__mro__) # Result (<class '__main__.canned'>, <class '__main__.warehouse'>, <class '__main__.supplier'>, <class '__main__.stock'>, <class 'object'>)
So this is interesting. The order followed is not what you might expect. Surely, it would be more efficient to search up one arm of the diamond, from canned, to warehouse, then to stock, before returning to search up the right hand side through supplier to stock again?
However, Python can’t work with ambiguity, and if it were to search as described you’d end up with the following order:
canned > warehouse > stock > object > supplier > stock > object
This simply doesn’t work, because when dealing with inheritance, precedence matters. This means that you can’t have a Grandparent class such as stock
, ahead of a Parent class such as supplier. Or the super class object, ahead of a Grandparent or Parent. This is why the method resolution order is defined as:
canned > warehouse > supplier > stock > object
This route complies with those two rules we introduced at the start of this article.
- Children will precede their parents in the sort process
- When a child class inherits from multiple parent and grandparent classes, the search order follows the order specified in the
__bases__
attribute.
Why does MRO matter?
Once your code begins to get more complex, with multiple levels of inheritance, hybrid structures and nested classes, life begins to get unwieldy. If classes use the same attribute or method names, you might get a nasty surprise when you call a method or attribute, only to have a completely unexpected value returned. You might also receive an error message when Python is unable to resolve ambiguity or conflict as follows.
class supplier(): level = 1 class distributor(): level = 1 class shop_warehouse(supplier, distributor): level = 2 class shop_dispatch(shop_warehouse): level = 3 class shelf(distributor, shop_dispatch): pass print(shelf.level) # Result TypeError: Cannot create a consistent method resolution order (MRO) for bases distributor, shop_dispatch
Why? Here’s the block diagram.
We know that a higher class can’t come before a lower class, yet that’s what we’re asking for when we created the shelf class using shelf(distributor, shop_dispatch)
. There are two solutions to this problem. One is to reverse the bracketed classes to read shelf(shop_dispatch, distributor)
as shown here. But you’re you’re patching over a trap that may arise with future code changes, leaving you with messy and possibly problematic code.
class supplier(): level = 1 class distributor(): level = 2 class shop_warehouse(supplier, distributor): level = 4 class shop_dispatch(shop_warehouse): level = 5 class shelf(shop_dispatch, distributor): pass print(shelf.level) # Result 5
The other method is to remove the distributor class as a parent of shelf
as all attributes and methods are available through the hierarchy. See the following code where we have removed the link to the distributor class from the shelf
class. To prove it works, we then added a method in the distributor class, instantiated an object using the shelf
class, then called the label method using the new object.
class supplier(): level = 1 class distributor(): level = 2 def label(self): print('Distributor label') class shop_warehouse(supplier, distributor): level = 4 class shop_dispatch(shop_warehouse): level = 5 class shelf(shop_dispatch): pass a4 = shelf() # Instantiated an object using class shelf a4.label() # Called the method label() which sits within the distributor class # Result Distributor label
Then if we call the __mro__
method on the shelf
class we see the following returned.
(<class '__main__.shelf'>, <class '__main__.shop_dispatch'>, <class '__main__.shop_warehouse'>, <class '__main__.supplier'>, <class '__main__.distributor'>, <class 'object'>)
Nice, neat and comprehensible code with a clearly defined method resolution order.
Summary
Today we highlighted the Method Resolution Order, or MRO, in Python, which is the order in which Python searches for classes, attributes and methods when dealing with multiple class inheritance, and we discussed the importance of such an order.
Three significant attributes, called __bases__
, __mro__
, and the Python in-built super class object, were reviewed and shown in action. We also introduced Python’s two rules when climbing the family tree looking for classes, methods, and attributes.
Finally, we showed where errors might occur when dealing with multiple inheritances, why they might happen, and how knowing the basics of MRO can assist us in avoiding them.
Many thanks for reading, and I trust the article has been of assistance.