Chapter 10: Understanding Method Resolution Order of Inheritance

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:

  • Understanding the MRO of a class
  • Understanding the impact of modifying the order of inheritance
  • Impact of unintended change of order in inheritance

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.

Technical requirements

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.

Understanding the MRO of a class

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:

  1. In the Branch class, let’s create attributes for branch ID, street, city, state and ZIP code, product, sales, and invoice. Let’s also create methods such as get_product (which returns the product), get_sales (which returns sales), and get_invoice (which returns the invoice). The following code represents the Branch class:

    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.

  1. Next, let’s call the mro method of the Branch class:

    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.

Understanding MRO in single inheritance

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:

  1. Before proceeding with the creation of the child class, let’s redefine the Branch class with suitable methods that can be used for testing this concept:

    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

  2. For this example, let’s create another class, named NYC, which inherits from the Branch class:

    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.

  1. Let’s look at what happens when a method required by NYC is not present in NYC but instead defined in its parent class. In the NYC class, calc_tax_nyc is the method that calculates tax for the NYC branch, and this method needs values for attributes such as branch, intercitybranch, product, and sales. The value for the intercitybranch attribute alone can be set within the NYC class using the set_management method, whereas the remaining attributes, such as branch, product, and sales, do not have a set method in NYC.
  2. Let’s start by creating a variable named intercitybranch and defining an instance for NYC:

    intercitybranch = {

        }

    branch_manhattan = NYC(intercitybranch)

  3. Let’s set the value for intercitybranch first, and then look at how to deal with the set methods for the remaining attributes:

    branch_manhattan.set_management({'regionalManager' : 'John M',

        'branchManager' : 'Tom H',

        'subbranch_id' : '2021-01' })

  4. The set methods required to set branch, product, and sales are available in the parent class of Branch. Since the MRO of the NYC class is to resolve from NYC followed by Branch followed by object, the set methods of Branch can now be called by NYC to set the values for branch, product, and sales as follows:

    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)

  5. Now that the required values are set, we are good to call the calc_tax_nyc method from the NYC class that inherited the Branch class:

    branch_manhattan.calc_tax_nyc()

  6. The selling price calculated using the tax rate and the other supporting values of branch, product, and sales set using the parent class is represented in the following output:

    ({'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.

Understanding MRO in multiple inheritances

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:

  1. The Product class will have a set of attributes followed by a method named get_product:

    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

  2. The Branch class will have a set of attributes followed by a method named get_branch:

    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

  3. Let’s next create a child class or subclass named Sales and inherit from the Product and Branch classes. Sales will have one attribute date and a get_sales method:

    class Sales(Product, Branch):

        date = '08/02/2021'

        def get_sales(self):

            return self.date, Product.get_product(self),

                   Branch.get_branch(self)

  4. The Sales class inherits Product followed by Branch:

    Sales.mro()

  5. Let’s look at the order of its method resolution:

    [__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.

  1. Let’s create another class (named Invoice) and inherit both Branch and Product in an order that's different from the inheritance of the Sales class:

    class Invoice(Branch, Product):

        date = '08/02/2021'

        def get_invoice(self):

            return self.date, Branch.get_branch(self),

                   Product.get_product(self)

  2. Let’s examine mro for the Invoice class:

    Invoice.mro()

  3. The mro for the Invoice class is represented in the following output:

    [__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.

Reviewing MRO in 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:

  1. Let’s first create a class named StoreCoupon, where we will be defining attributes for a store such as product name, product category, the brand of the product, store name where the product is sold, expiry date of the product, and quantity to be purchased to get a coupon.
  2. We will then define a method named generate_coupon, where we will be generating two coupons for the product with random coupon ID values and all the details of the product and its store:

    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('***********------------------

                       **************')

  3. Let’s now define a class, SendStoreCoupon, that inherits StoreCoupon and does not add any methods or attributes to it:

    class SendStoreCoupon(StoreCoupon):

        pass

    SendStoreCoupon.mro()

  4. The MRO of this class is represented in the following output:

    [__main__.SendStoreCoupon, __main__.StoreCoupon, object]

  5. The methods in SendStoreCoupon are resolved first, followed by the methods in the StoreCoupon class, followed by object.
  6. Let’s add one more level of inheritance by defining another class, named SendCoupon, and inheriting it from the SendStoreCoupon classes:

    class SendCoupon(SendStoreCoupon):

        pass

    SendCoupon.mro()

  7. The MRO of this class is represented in the following output:

    [__main__.SendCoupon,

      __main__.SendStoreCoupon,

    __main__.StoreCoupon,

    object]

  8. In the preceding output, the methods are resolved from SendCoupon followed by SendStoreCoupon followed by StoreCoupon followed by object.
  9. Let’s create an object for the SendCoupon class and call the generate_coupon method:

    coupon = SendCoupon()

    coupon.generate_coupon()

  10. The SendCoupon class does not have a definition for the generate_coupon method and so, as per the MRO, the parent class or superclass’ SendStoreCoupon method will be called, as in the following output:

    ***********------------------**************

    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.

Understanding the importance 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:

  1. First, we will be creating another class, named ManufacturerCoupon, where we will be defining attributes for a manufacturer such as the product name, product category, brand of the product, manufacturer name where the product is sold, expiry date of the product, and quantity to be purchased to get a coupon.
  2. We will then define a method named generate_coupon, where we will be generating two coupons for the product with random coupon ID values and all the details of the product and its manufacturer:

    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('***********------------------

                       **************')

  3. Let’s also define the SendCoupon class with two parent classes—ManufacturerCoupon and SendStoreCoupon:

    class SendCoupon(ManufacturerCoupon,SendStoreCoupon):

        pass

    SendCoupon.mro()

  4. The MRO of the class is represented in the following output:

    [__main__.SendCoupon,

    __main__.ManufacturerCoupon,

    __main__.SendStoreCoupon,

    __main__.StoreCoupon,

    object]

  5. Let’s further create an object for the class and call the generate_coupon method:

    coupon = SendCoupon()

    coupon.generate_coupon()

  6. The generate_coupon method generated coupons for the manufacturer in this example since the first parent that has the generate_coupon method definition is ManufacturerCoupon. The following coupons are generated from the generate_coupon method:

    ***********------------------**************

    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

    ***********------------------**************

  7. Let’s further change the order of inheritance in the SendCoupon class and look at how the methods are resolved:

    class SendCoupon(SendStoreCoupon,ManufacturerCoupon):

        pass

    SendCoupon.mro()

  8. The MRO of the class is represented in the following output:

    [__main__.SendCoupon,

    __main__.SendStoreCoupon,

    __main__.StoreCoupon,

    __main__.ManufacturerCoupon,

    object]

  9. Let’s further create an object for the class and call the generate_coupon method:

    coupon = SendCoupon()

    coupon.generate_coupon()

  10. The generate_coupon method generated coupons for the store in this example since the first parent that has the generate_coupon method definition is SendStoreCoupon, which in turn inherits the method from its StoreCoupon parent class, as represented in the following output:

    ***********------------------**************

    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.

Impact of unintended change of order in inheritance

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:

  1. Let’s start by creating a class named CommonCounter that initializes with two attributes, items and name. Let’s also add two methods to this class, return_cart (which returns the items in the cart) and goto_counter (which returns the name of the counter). This is how the code looks:

    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()

  2. The MRO of the class is represented in the following output:

    [__main__.CommonCounter, object]

  3. Let’s now create another class, named CheckItems, which is also going to be a parent class in multilevel inheritance applied in this section. This class will have one attribute named item_type and one method named review_items that returns the name of the counter based on the type of items in the cart:

    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()

  4. The MRO of the class is represented in the following output:

    [__main__.CheckItems, object]

  5. On the second level of inheritance, let’s create a class named ElectronicsCounter, which inherits from the CommonCounter and CheckItems classes, in that order:

    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()

  6. The MRO of the class is represented in the following output:

    [__main__.ElectronicsCounter,

    __main__.CommonCounter,

    __main__.CheckItems,

    object]

  7. On the second level of inheritance, let’s also create a class named VegeCounter, which inherits from the CheckItems and CommonCounter classes, in that order:

    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()

  8. The MRO of the class is represented in the following output:

    [__main__.VegeCounter,

    __main__.CheckItems,

    __main__.CommonCounter,

    object]

  9. Let’s now create another class, named ScanCode, which inherits the ElectronicsCounter and VegCounter classes:

    class ScanCode(ElectronicsCounter,VegeCounter):

        pass

The preceding code results in the following error message:

Figure 10.1 – MRO error

Figure 10.1 – MRO error

  1. Even though the MRO of the class is ScanCode followed by ElectronicsCounter followed by VegeCounter followed by CommonCounter followed by CheckItems followed by object, the MROs of the CommonCounter and CheckItems base classes are reversed. Therefore, the overall class definition throws an error in this scenario.

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.

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset