How to Manage Multiple Inheritance in Python
In this guide, we'll learn how to use multiple inheritance in Python and make it sustainable.
Photo by Hitesh Choudhary on Unsplash
High-level object-oriented programming languages such as C# and Java do not support multiple inheritance. For a good reason too. It prevents ambiguation and facilitates strong typing and encapsulation. This ultimately results in cleaner code and less confusing class/object model structures. However, multiple inheritance allows for more flexible design patterns.
It can also be very convenient, allowing programmers to write less code and push to production quicker. Nevertheless, when you're working with large, complicated applications (especially ones that pertain to machine learning), multiple inheritance can be difficult to manage and maintain. In this guide, we'll learn how to use multiple inheritance and make it sustainable.
Multiple Inheritance in Python
Much like C++, classes in Python can be derived from multiple classes (instead of just one). The deriving class inherits all the parent classes' features (variables and methods/functions). In actuality, defining a class with multiple inheritances is really no different from defining one with single inheritance. Here is an example to help illustrate this fact:
class Parent: pass class Child(Parent): pass
class Parent1: pass class Parent2: pass class Child(Parent1, Parent2): pass
Since the Child class is derived from two classes, it inherits features from both classes. Of course, you can inherit from more than two classes. Theoretically, there is no limit to the number of classes you can use to construct a child class.
As with most object-oriented programming languages, Python features multi-level inheritance. If your child class is derived from a class that inherits its features from another class, it will inherit all features from both classes.
As you can see from the above diagram, the class lowest on the class hierarchy inherits all the functions/methods and variables from the two classes above it. Combined with multiple inheritance, multi-level inheritance can offer developers flexible design options. They can also increase code reusability. However, they can also make class structures more complicated.
For instance, if you're using a feature (method or variable) with an identical name and structure to a method in a superclass or base class, which method will be called first from the subclass? Your class hierarchy may have functions and variables with the same names and signatures. How does Python handle this?
Method Resolution Order
Again, as with most OO programming languages, all classes are derived from a singular class. In Python, this is known as the object class. Thus, by default, every class you create is a child/deriving/subclass of the Object class. When we're trying to understand the call order of methods and variables in Python, you must always remember this information when trying to map out the method call order of your program.
Again because Python features multiple and multi-level inheritance, it requires a system to resolve inheritance conflicts where the same property has different definitions in multiple base/superclasses. The best way to understand this is by looking at a coding example:
class A: num = 10 class B(A): pass class C(A): num = 1 class D(B,C): pass
Class A inherits from the Object class (as all classes in Python do). Class B uses Class A as a base class. It has no features of its own. However, it does inherit the num class object attribute from class A (as well as other features from the object class).
Class C is also a subclass of Class A and defines a class object attribute that shares a name with one defined in Class A. Class D inherits from D and C but defines no attributes of its own. The relationship looks a lot like this:
The above diagram illustrates what is referred to as the diamond problem (or deadly diamond of death) in computing. Method resolution order (MRO) is a rule to solve this problem. It uses MRO to organize the method and attribute calls.
Note: Diagram 2 isn’t a strict dependency chart. Rather, it illustrates the flow of inheritance.
Let's add a method that references the num attribute inherited by the D class. This method should be called outside all of the classes.
If we run it along with our previous code, it should return the number 1 as our output. You may be able to tell that it is the value of the num attribute in Class C. But why would it return that specific attribute and value?
MRO conducts a property search starting from the current class before moving to the parent classes. If there are multiple parent classes, MRO will search them from left to right. So in our example the search will be conducted as follows: D -> B -> C -> A -> object class.
While it would be helpful to meticulously construct a dependency graph or class diagram as you would a business plan, it's unnecessary. You can simply use Python’s built-in mro() method to find the resolution order.
If we run the mro() method on the D class and print out the output (i.e. print(D.mro())), the console should display these results:
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
Once again, it can be translated as: D -> B -> C -> A -> object class. Nevertheless, this is easy to understand for simple diamond problems such as the one above. However, what if you get:
Diagram 3 (Source: Wikimedia Commons (CC) license)
MRO uses an algorithm called Depth First Search to resolve all method and attribute calls. The latest version of Python revised how MRO works. It's uncertain if Python's classpath rules and MRO will be refined in future versions too. Therefore, many developers (including) myself advise that you stay away from constructing programs or algorithms with too much multiple and multi-level inheritance.
If you're writing code that has a dependency structure similar to the diagram above, it can be argued that you've written bad code. Essentially, the examples in this guide hardly had any real functional code between the classes. Yet, the structures were still confusing. Now, imagine if you defined methods and attributes with identical structures and began calling them...
While MRO exists and every Python developer should understand it, your code should not be structured like Diagram 3 is. However, as a developer (especially one working with machine learning), you're bound to run into class structures with complicated dependencies. Using the mro() method should help you debug, untangle and understand code that you may not have written.
Python's main advantage over other OO programming languages is its flexibility and freedom. However, its fluidity can be seen as a disadvantage too. There is too much room for error and inefficient code. Thus, programmers, especially those working with machine learning and deep learning, should learn and apply the most optimal design patterns. Notwithstanding, to be safe, you may elect to avoid using multiple inheritance in your code altogether. However, if you decide to implement it, do it mindfully and sparingly.
Nahla Davies is a software developer and tech writer. Before devoting her work full time to technical writing, she managed — among other intriguing things — to serve as a lead programmer at an Inc. 5,000 experiential branding organization whose clients include Samsung, Time Warner, Netflix, and Sony.