In this chapter, we will look at the concept of method resolution order (MRO) in Python 3 and how it works on inheritance.
As the name suggests, MRO is the order in which methods of a class get resolved while calling them in a program.
Throughout this chapter, we will look at understanding the MRO through a few examples, how method resolution can go wrong, and how the current Python 3 implementation handles methods defined in a class. We will be making use of MRO throughout this chapter to understand the behavior of code while inheritance is implemented in Python 3.
Why should we understand MRO? In scenarios where we are using multiple classes in Python code, we need to inherit methods from multiple parent classes or superclasses. Understanding the order in which the methods would get resolved from the existing class to its parent class helps in avoiding incorrect method calls. This in turn helps in avoiding incorrect results in the algorithm of Python code.
In this chapter, we will be taking a look at the following main topics:
By the end of this chapter, you should be able to get an understanding of how methods are resolved in Python class hierarchy, understand how methods are processed in multiple inheritances, and write the methods on your own with the knowledge of how they would get resolved.
The code examples shared in this chapter are available on GitHub under the code for this chapter here: https://github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter10.
In this section, let’s explore how methods are resolved in a class that has no inheritance specified within its code. A class by default in Python 3 is inherited by object. To understand how MRO works on a class that has no parent class, looking at it in its simplest form is the easiest approach. We will then see how MRO works on a class with single, multiple, and multilevel inheritance.
In this example, let’s create a class for a branch of ABC Megamart as follows:
class Branch:
def __init__(self, branch_id, branch_street,
branch_city, branch_state,
branch_zip, product, sales, invoice):
self.branch_id = branch_id
self.branch_street = branch_street
self.branch_city = branch_city
self.branch_state = branch_state
self.branch_zip = branch_zip
self.product = product
self.sales = sales
self.invoice = invoice
def get_product(self):
return self.product
def get_sales(self):
return self.sales
def get_invoice(self):
return self.invoice
There are five attributes and three methods in the preceding class. The MRO for the preceding class can be reviewed by calling a built-in method on the class, known as mro.
Branch.mro()
The mro of the Branch class is represented as follows:
[__main__.Branch, object]
In the preceding output, we can see that the Branch class did not have any explicit definition of a superclass or parent class, and so it is, by default, inherited from the object.
In this section, we understood the concept of MRO along with an example of how to look at the MRO of a class. Now, let’s look further to see how MRO works on a class that has a single parent class or superclass.
When a class inherits one parent class or superclass, it is single inheritance. Let’s look at how methods are resolved in the case of the Branch class example when it becomes a parent class:
class Branch:
def __init__(self, branch, sales, product):
self.branch = branch
self.sales = sales
self.product = product
def set_branch(self, value):
self.branch = value
def set_sales(self, value):
self.sales = value
def set_product(self, value):
self.product = value
def calc_tax(self):
branch = self.branch
product = self.product
sales = self.sales
pricebeforetax = sales['purchase_price'] +
sales['purchase_price'] *
sales['profit_margin']
finalselling_price = pricebeforetax +
(pricebeforetax * sales['tax_rate'])
sales['selling_price'] = finalselling_price
return branch, product, sales
class NYC(Branch):
def __init__(self, intercitybranch):
self.intercitybranch = intercitybranch
def set_management(self, value):
self.intercitybranch = value
def calc_tax_nyc(self):
branch = self.branch
intercitybranch = self.intercitybranch
product = self.product
sales = self.sales
pricebeforetax = sales['purchase_price'] +
sales['purchase_price'] *
sales['profit_margin']
finalselling_price = pricebeforetax +
(pricebeforetax * (sales['tax_rate'] +
sales['local_rate']))
sales['selling_price'] = finalselling_price
return branch, intercitybranch, product,
sales
NYC.mro()
In the preceding code, we have the NYC class inherited from the Branch class, and the NYC class has two methods defined. The set_management method returns the value stored in intercitybranch, and the calc_tax_nyc method calculates tax for NYC.
The MRO of the NYC class is represented in the following output:
[__main__.NYC, __main__.Branch, object]
The methods present in NYC will be resolved first, followed by the methods of Branch and then the methods of object.
intercitybranch = {
}
branch_manhattan = NYC(intercitybranch)
branch_manhattan.set_management({'regionalManager' : 'John M',
'branchManager' : 'Tom H',
'subbranch_id' : '2021-01' })
branch = {'branch_id' : 2021,
'branch_street' : '40097 5th Main Street',
'branchBorough' : 'Manhattan',
'branch_city' : 'New York City',
'branch_state' : 'New York',
'branch_zip' : 11007}
product = {'productId' : 100002,
'productName' : 'WashingMachine',
'productBrand' : 'Whirlpool'
}
sales = {
'purchase_price' : 450,
'profit_margin' : 0.19,
'tax_rate' : 0.4,
'local_rate' : 0.055
}
branch_manhattan.set_branch(branch)
branch_manhattan.set_product(product)
branch_manhattan.set_sales(sales)
branch_manhattan.calc_tax_nyc()
({'branch_id': 2021,
'branch_street': '40097 5th Main Street',
'branchBorough': 'Manhattan',
'branch_city': 'New York City',
'branch_state': 'New York',
'branch_zip': 11007},
{'regionalManager': 'John M',
'branchManager': 'Tom H',
'subbranch_id': '2021-01'},
{'productId': 100002,
'productName': 'WashingMachine',
'productBrand': 'Whirlpool'},
{'purchase_price': 450,
'profit_margin': 0.19,
'tax_rate': 0.4,
'local_rate': 0.055,
'selling_price': 779.1525})
In this section, we looked at how MRO works in classes that have a single inheritance. Now, let’s look at what happens when a class inherits from two classes.
In this section, we will look at inheriting from more than one superclass or parent class and its corresponding MRO.
For this example, let’s create two parent classes, Product and Branch, as follows:
class Product:
_product_id = 100902
_product_name = 'Iphone X'
_product_category = 'Electronics'
_unit_price = 700
def get_product(self):
return self._product_id, self._productName, self._product_category, self._unit_price
class Branch:
_branch_id = 2021
_branch_street = '40097 5th Main Street'
_branch_borough = 'Manhattan'
_branch_city = 'New York City'
_branch_state = 'New York'
_branch_zip = 11007
def get_branch(self):
return self._branch_id, self._branch_street,
self._branch_borough, self._branch_city,
self._branch_state, self._branch_zip
class Sales(Product, Branch):
date = '08/02/2021'
def get_sales(self):
return self.date, Product.get_product(self),
Branch.get_branch(self)
Sales.mro()
[__main__.Sales, __main__.Product, __main__.Branch, object]
In the preceding output, the methods are resolved in the order of Sales followed by Product followed by Branch followed by object. If a method called by an object of the Sales class is not present in Sales, the MRO algorithm searches for it within the Product class followed by the Branch class.
class Invoice(Branch, Product):
date = '08/02/2021'
def get_invoice(self):
return self.date, Branch.get_branch(self),
Product.get_product(self)
Invoice.mro()
[__main__.Invoice, __main__.Branch, __main__.Product, object]
In the preceding output, the methods are resolved in the order of Invoice followed by Branch followed by Product followed by object. If a method called by an object of the Invoice class is not present in Invoice, the MRO algorithm searches for it within the Branch class followed by the Product class.
In the case of multiple inheritances, we reviewed how the order of method resolution changes when the order of inheriting superclasses or parent classes changes in Python 3.
Now, let’s look at what happens to MRO in the case of multilevel inheritance.
Classes in Python can also inherit from superclasses at multiple levels, and the MRO gets more complicated as the number of superclasses or parent classes increases. In this section, let’s look at the order of method resolution for such multiple inheritances with a few more examples.
In this example, we will perform the following steps:
class StoreCoupon:
productName = "Strawberry Ice Cream"
product_category = "Desserts"
brand = "ABCBrand3"
store = "Los Angeles Store"
expiry_date = "10/1/2021"
quantity = 10
def generate_coupon(self):
import random
coupon_id = random.sample(range(
100000000000,900000000000),2)
for i in coupon_id:
print('***********------------------**************')
print('Product:', self.productName)
print('Product Category:',
self.product_category)
print('Coupon ID:', i)
print('Brand:', self.brand)
print('Store:', self.store)
print('Expiry Date:', self.expiry_date)
print('Quantity:', self.quantity)
print('***********------------------
**************')
class SendStoreCoupon(StoreCoupon):
pass
SendStoreCoupon.mro()
[__main__.SendStoreCoupon, __main__.StoreCoupon, object]
class SendCoupon(SendStoreCoupon):
pass
SendCoupon.mro()
[__main__.SendCoupon,
__main__.SendStoreCoupon,
__main__.StoreCoupon,
object]
coupon = SendCoupon()
coupon.generate_coupon()
***********------------------**************
Product: Strawberry Ice Cream
Product Category: Desserts
Coupon ID: 532129664296
Brand: ABCBrand3
Store: Los Angeles Store
Expiry Date: 10/1/2021
Quantity: 10
***********------------------**************
***********------------------**************
Product: Strawberry Ice Cream
Product Category: Desserts
Coupon ID: 183336814176
Brand: ABCBrand3
Store: Los Angeles Store
Expiry Date: 10/1/2021
Quantity: 10
***********------------------**************
In this example, we looked at how the methods are resolved from one level of inheritance to the other.
Now, let’s look further into the impact of modifying the order of inheritance.
In this section, we will look at inheriting from more than one parent class. We will see what happens to the method resolution when the order of the parent class changes in addition to the SendStoreCoupon class that was created in the preceding section:
class ManufacturerCoupon:
productName = "Strawberry Ice Cream"
product_category = "Desserts"
brand = "ABCBrand3"
manufacturer = "ABC Manufacturer"
expiry_date = "10/1/2021"
quantity = 10
def generate_coupon(self):
import random
coupon_id = random.sample(range(
100000000000,900000000000),2)
for i in coupon_id:
print('***********------------------**************')
print('Product:', self.productName)
print('Product Category:',
self.product_category)
print('Coupon ID:', i)
print('Brand:', self.brand)
print('Manufacturer:', self.manufacturer)
print('Expiry Date:', self.expiry_date)
print('Quantity:', self.quantity)
print('***********------------------
**************')
class SendCoupon(ManufacturerCoupon,SendStoreCoupon):
pass
SendCoupon.mro()
[__main__.SendCoupon,
__main__.ManufacturerCoupon,
__main__.SendStoreCoupon,
__main__.StoreCoupon,
object]
coupon = SendCoupon()
coupon.generate_coupon()
***********------------------**************
Product: Strawberry Ice Cream
Product Category: Desserts
Coupon ID: 262335232934
Brand: ABCBrand3
Manufacturer: ABC Manufacturer
Expiry Date: 10/1/2021
Quantity: 10
***********------------------**************
***********------------------**************
Product: Strawberry Ice Cream
Product Category: Desserts
Coupon ID: 752333180295
Brand: ABCBrand3
Manufacturer: ABC Manufacturer
Expiry Date: 10/1/2021
Quantity: 10
***********------------------**************
class SendCoupon(SendStoreCoupon,ManufacturerCoupon):
pass
SendCoupon.mro()
[__main__.SendCoupon,
__main__.SendStoreCoupon,
__main__.StoreCoupon,
__main__.ManufacturerCoupon,
object]
coupon = SendCoupon()
coupon.generate_coupon()
***********------------------**************
Product: Strawberry Ice Cream
Product Category: Desserts
Coupon ID: 167466225705
Brand: ABCBrand3
Store: Los Angeles Store
Expiry Date: 10/1/2021
Quantity: 10
***********------------------**************
***********------------------**************
Product: Strawberry Ice Cream
Product Category: Desserts
Coupon ID: 450583881080
Brand: ABCBrand3
Store: Los Angeles Store
Expiry Date: 10/1/2021
Quantity: 10
***********------------------**************
In this section, we understood the impact of the order in which a child class resolved the parent classes or the superclasses.
With this understanding, let’s look at what happens when the inheritance becomes even more complex and where it can lead to errors.
In this section, we will be looking at examples that demonstrate how important the order of inheritance is to resolve the methods in the case of multilevel inheritance, and what happens when the order changes in one of the parent or superclasses unintentionally.
This is how it works:
class CommonCounter():
def __init__(self,items,name):
self.items = items
self.name = name
def return_cart(self):
cartItems = []
for i in self.items:
cartItems.append(i)
return cartItems
def goto_counter(self):
countername = self.name
return countername
CommonCounter.mro()
[__main__.CommonCounter, object]
class CheckItems():
def __init__(self, item_type = None):
self.item_type = item_type
def review_items(self, item_type = None):
veg_cart = ['Vegetables', 'Dairy', 'Fruits']
if (item_type == 'Electronics'):
print("Move to Electronics Counter")
elif (item_type in veg_cart):
print("Move to Vege Counter")
CheckItems.mro()
[__main__.CheckItems, object]
class ElectronicsCounter(CommonCounter,CheckItems):
def __init__(status = None):
self.status = status
def test_electronics(self):
teststatus = []
for i in self.status:
teststatus.append(i)
return teststatus
ElectronicsCounter.mro()
[__main__.ElectronicsCounter,
__main__.CommonCounter,
__main__.CheckItems,
object]
class VegeCounter(CheckItems,CommonCounter):
def __init__(weights = None):
self.weights = weights
def weigh_items(self):
item_weight = dict(zip(self.items,
self.weights))
return item_weight
VegeCounter.mro()
[__main__.VegeCounter,
__main__.CheckItems,
__main__.CommonCounter,
object]
class ScanCode(ElectronicsCounter,VegeCounter):
pass
The preceding code results in the following error message:
Figure 10.1 – MRO error
This example demonstrates the impact of unintended change in the order of inheritance. It is important to ensure that the order of classes is correct while defining classes with multilevel inheritance in Python so that the MRO is consistent for base classes.
In this chapter, we have learned about the concept of method resolution by exploring the MRO method in Python 3. We also inspected the MRO of Python code by implementing different types of inheritance. We understood the impact of MRO by modifying the order of inheritance at various levels for multiple classes from our core example.
Similar to other chapters covered in this book, this chapter explains that the MRO also focuses on metaprogramming and its impact on Python code.
In the next chapter, we will be looking at the concept of dynamic objects, with some other interesting examples.