Tutorials
python

Intro to Multiple Inheritance & super()

A Pythonista's introductory guide to multiple inheritance, the super() function, & how to navigate the diamond problem.

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 replace ParentName.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.
Want to leave a comment?