Intro to Multiple Inheritance & super()
Quick overview of inheritance
As you grow your Python projects and packages, you'll inevitably want to utilize classes and apply the DRY (don't-repeat-yourself) principle while doing so. Class inheritance is a fantastic way to create a class based on another class in order to stay DRY. This post will cover more advanced concepts of inheritance, and basic inheritance won't be covered in depth. We'll go over a quick intro, but there are much better, detailed introductions out there. Here are some resources to get started: Object-Oriented Programming in Python course & Python Object-Oriented Programming (OOP): Tutorial.
So what is class inheritance? Similarly to genetics, a child class can 'inherit' attributes and methods from a parent. Let's jump right into some code for an example. In the below code block we'll demonstrate inheritance with a Child
class inheriting from a Parent
class.
Input
class Parent:
def __init__(self):
self.parent_attribute = 'I am a parent'
def parent_method(self):
print('Back in my day...')
# Create a child class that inherits from Parent
class Child(Parent):
def __init__(self):
Parent.__init__(self)
self.child_attribute = 'I am a child'
# Create instance of child
child = Child()
# Show attributes and methods of child class
print(child.child_attribute)
print(child.parent_attribute)
child.parent_method()
Output
I am a child
I am a parent
Back in my day...
We see that the Child
class 'inherited' attributes and methods from the Parent
class. Without any work on our part, the Parent.parent_method
is a part of the Child
class. To get the benefits of the Parent.__init__()
method we needed to explicitly call the method and pass self
. This is because when we added an __init__
method to Child
, we overwrote the inherited __init__
.
With that brief, very non-comprehensive overview out of the way lets jump into the meat of the post.
Intro to super
In the simplest case, the super
function can be used to replace the explicit call to Parent.__init__(self)
. Our intro example from the first section can be rewritten with super
as seen below. Note, that the below code block is written in Python 3, earlier versions use a slightly different syntax. Additionally, the output has been omitted since it's identical to the first code block.
class Parent:
def __init__(self):
self.parent_attribute = 'I am a parent'
def parent_method(self):
print('Back in my day...')
# Create a child class that inherits from Parent
class Child(Parent):
def __init__(self):
super().__init__()
self.child_attribute = 'I am a parent'
# Create instance of child
child = Child()
# Show attributes and methods of child class
print(child.child_attribute)
print(child.parent_attribute)
child.parent_method()
To be honest, super
in this case gives us little, if any, advantage. Depending on the name of our parent class we might save some keystrokes, and we don't have to pass self
to the call to __init__
. Below are some pros and cons of the use of super
in single inheritance cases.
Cons
It can be argued that using super
here makes the code less explicit. Making code less explicit violates The Zen of Python, which states, "Explicit is better than implicit."
Pros
There is a maintainability argument that can be made for super
even in single inheritance. If for whatever reason your child class changes its inheritance pattern (i.e., parent class changes or there's a shift to multiple inheritance) then there's no need find and replace all the lingering references to ParentClass.method_name()
; the use of super
will allow all the changes to flow through with the change in the class
statement.
super
and multiple inheritance
Before we get into multiple inheritance and super
... Warning, this can get pretty weird and complicated.
First off, what is multiple inheritance? So far the example code has covered a single child class inheriting from a single parent class. In multiple inheritance, there's more than one parent class. A child class can inherit from 2, 3, 10, etc. parent classes.
Here is where the benefits of super
become more clear. In addition to saving keystrokes of referencing the different parent class names, there are nuanced benefits to using super
with multiple inheritance patterns. In short, if you're going to use multiple inheritance, use super
.
Multiple inheritance without super
Let's look at an example of multiple inheritance that avoids modifying any parent methods and in turn avoids super
.
Input
class B:
def b(self):
print('b')
class C:
def c(self):
print('c')
class D(B, C):
def d(self):
print('d')
d = D()
d.b()
d.c()
d.d()
Output
b
c
d
Multiple-resolution order
This output isn't too surprising given the concept of multiple inheritance. D
inherited the methods x
and z
from its parent classes, and everything is good in the world... for now.
So what if both B
and C
both had a method with the same name? This is where a concept called 'multiple-resolution order' comes into play or MRO for short. The MRO of a child class is what decides where Python will look for a given method, and which method will be called when there's a conflict.
Let's look at an example.
Input
class B:
def x(self):
print('x: B')
class C:
def x(self):
print('x: C')
class D(B, C):
pass
d = D()
d.x()
print(D.mro())
Output
x: B
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class 'object'>]
When we call the inherited x
method, we only see the output inherited from B
. We can see the MRO of our D
class by calling the mro
class method. From the D.mro()
output we learn the following: our program will try to call D
methods by default, then resort to B
, then C
, and finally object
. If it's not found in any of those places, then we'll get the error that D
doesn't have the method we asked for.
It's worth noting that by default, every class inherits from object
, and it's on the tail-end of every MRO.
Multiple inheritance, super
, and the diamond problem
Below is an example of using super to handle MRO of init in a way that's beneficial. In the example, we create a series of text processing classes and combine their functionality in another class with multiple inheritance. We'll create 4 classes, and the structure for inheritance will follow the structure in the below diagram.
Note: This structure is for illustrative purposes, and, barring restraints, there would be better ways to implement this logic.
This is actually an example of the 'diamond problem' of multiple inheritance. Its name is of course based on the shape of its design, and the fact that it's a fairly confusing problem.
Below the design is written out with the use of super
.
Input
class Tokenizer:
"""Tokenize text"""
def __init__(self, text):
print('Start Tokenizer.__init__()')
self.tokens = text.split()
print('End Tokenizer.__init__()')
class WordCounter(Tokenizer):
"""Count words in text"""
def __init__(self, text):
print('Start WordCounter.__init__()')
super().__init__(text)
self.word_count = len(self.tokens)
print('End WordCounter.__init__()')
class Vocabulary(Tokenizer):
"""Find unique words in text"""
def __init__(self, text):
print('Start init Vocabulary.__init__()')
super().__init__(text)
self.vocab = set(self.tokens)
print('End init Vocabulary.__init__()')
class TextDescriber(WordCounter, Vocabulary):
"""Describe text with multiple metrics"""
def __init__(self, text):
print('Start init TextDescriber.__init__()')
super().__init__(text)
print('End init TextDescriber.__init__()')
td = TextDescriber('row row row your boat')
print('--------')
print(td.tokens)
print(td.vocab)
print(td.word_count)
Output
Start init TextDescriber.__init__()
Start WordCounter.__init__()
Start init Vocabulary.__init__()
Start Tokenizer.__init__()
End Tokenizer.__init__()
End init Vocabulary.__init__()
End WordCounter.__init__()
End init TextDescriber.__init__()
--------
['row', 'row', 'row', 'your', 'boat']
{'boat', 'your', 'row'}
5
First off, we see the TextDescriber
class has inherited all the attributes of the class family tree. Thanks to multiple inheritance we can 'combine' the functionality of more than one class.
Let's now discuss the printouts that came from the class's init
methods:
Each __init__
method was called once and only once.
The TextDescriber
class inherited from 2 classes that inherit from Tokenizer
. Why was Tokenizer.__init__
not called twice?
If we replaced all of our calls to super
with the old fashioned way, we would end up with 2 calls to Tokenizer.__init__
. The calls to super
'think' through our pattern a little bit more and skips the extra trip to A
.
Each __init__
method was started before any of the others were finished.
The order of the starts and finishes of each __init__
is worth noting in case you're attempting to set an attribute that has a naming conflict with another parent class. The attribute will be overwritten, and it can become very confusing.
In our case, we avoided naming conflicts with inherited attributes, so everything is working as expected.
To reiterate, the diamond problem can get complicated fast and lead to unexpected results. With most cases in programming, it's best to avoid complicated designs.
What we learned
- We learned about the
super
function and how it can be used to replaceParentName.method
in single inheritance. This can be a more maintainable practice. - We learned about multiple inheritance and how we can pass on the functionality of multiple parent classes to a single child class.
- We learned about multiple-resolution order and how it decides what happens in multiple inheritance when there's a naming conflict between parent methods.
- We learned about the diamond problem and saw an example of how the use of
super
navigates the diamond.