Python Multiple inheritance and super() function for dummies
Multiple inheritance
Multiple inheritance is a feature in which a class can inherit characteristics from more than one parent class.
If we want to add the same methods to a number of classes we can do that by copying methods to each class, but this is against DRY principle.
Also we can do that with decorators but this is not convenient for big amount of application logic.
The better solution in this case - use mixin, for Python it’s multiple inheritence.
Puzzle
First parent of PlatypusMammalFirst
, Mammal
,
does not change the method we are looking for.
But the second - Bird
, does. What lay_eggs()
will print?
And what it will print for PlatypusBirdFirst
?
Solution see below.
Diamond problem
The puzzle above based on (diamond problem), this is feature of multiple inheritance as it is, not for Python only.
As you can see on picture, if a number of classes has the same parent and the same child, inheritance tree will have ‘diamonds’ (rhombs).
In Python 3 all classes without parent in fact inherits from
object
, so any case of multiple inheritance in Python 3 has diamonds in
inheritance tree because of the common parent - object
.
C3 Algorithm Python 3 (MRO)
To look for inherited methods and attributes, Python use C3 MRO algorithm.
MRO stands for “method resolution order”.
Very simple C3 explanation:
-
add to the list all object’s parents
- to the end of the list add parent’s parents
- repeat
-
if some class added more ones we remove all the occurrences except last one.
As a result we have search order in which we look for inherited methon by parent layers - we do not look deeper before we look in all upper layer parents.
For example for class Platypus
, MRO: [Mammal, Bird, Dinosaur, Vertebrate]
.
So we look in Mammal
, Bird
, Dinosaur
first.
And only after that - in Vertebrate
.
C3 gives you overridden method if it is overridden in any of ancestors despite their order in inheritance list of your class.
So the puzzle solution for Python 3 - True
in both cases, we never get
None
from Vertebrate
.
Python 2 use other MRO algorithm (deep first
).
It drills deeper and deeper to the end of hierarchy for each parent by their
order in inheritance list.
Python 2 MRO: [Mammal, Vertebrate, Bird, Dinosaur]
.
So after Mammal
if will look into Vertebrate
.
And puzzle solution for Python 2 - None
and True
.
But in Python 2 we can use new-style classes
and in this case it use the
same C3 MRO
as Python 3. To use new-style
classes in Python 2 you should
inherit Vertebrate
from object
.
In Python 3 this is default - if a class inheritance list is empty
the class inherits from object
.
With MRO as in Python 2 (deep first
), we could not use multiple inheritance
at all because of this Python 3 feature - default parent object
.
So in Python 3 we always have diamond in inheritance tree for objects with
multiple inheritance because all classes have the same grad-..-grand parent.
super() function
Function super() implements cooperative inheritance.
Do not be fooled by the name - super()
it is not class parent!
This is class next by MRO list.
As we will see below it can be sibling class. And a class behaviour will change if we add it to different classes.
In some cases this is just intuitively right and good. But that can be very confusing in other cases and can be a source of hard to discover bug.
Cooperative multiple inheritance with super()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Vertebrate:
def __init__(self):
print('Vertebrate.__init__()')
class Bird(Vertebrate):
def __init__(self):
print('Bird.__init__()')
super().__init__()
class Mammal(Vertebrate):
def __init__(self):
print('Mammal.__init__()')
super().__init__()
class Platypus(Mammal, Bird):
def __init__(self):
print('Platypus.__init__()')
super().__init__()
duckbill = Platypus()
Platypus.__init__()
Mammal.__init__()
Bird.__init__()
Vertebrate.__init__()
‘Cooperative’ means everybody has to follow rules.
If any of child class won’t call super()
), parent method won’t be
called at all even if we see that some other child do call parent.
For example lets remove super()
call from Bird
(line 8 in the listing above).
We will have different results:
Platypus.__init__()
Mammal.__init__()
Bird.__init__()
This is because in fact Mammal.__init__
call next in MRO list class
(Bird
), and not the parent Mammal
(Vertebrate
).
The parent was called by Bird
, but we removed that call.
In the code below I added args into __init__
, so the error looks more obvious.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Vertebrate:
def __init__(self):
print('Vertebrate.__init__()')
class Bird(Vertebrate):
def __init__(self, beak_length):
print('Bird.__init__()')
super().__init__()
class Mammal(Vertebrate):
def __init__(self, hair_length):
print('Mammal.__init__()')
super().__init__()
class Platypus(Mammal, Bird):
def __init__(self):
print('Platypus.__init__()')
super().__init__(1)
duckbill = Platypus()
Platypus.__init__()
Mammal.__init__()
...
File "animal_class_tree_arguments.py", line 13, in __init__
super().__init__()
TypeError: __init__() missing 1 required positional argument: 'beak_length'
Now we see that in Mammal
the line super().__init__()
calls Bird.__init__
.
Remarks about super()
Another contr-intuitive super()
feature is unsupported
binary operations -
subscriptions
etc.
Even if parent implements all necessary
“magic methods”
it won’t work in super
calls.
Example below illustrates that for magic method
__getitem__
,
for operator []
.
It works in child but not in super()
:
kid[0]: 0
...
TypeError: 'super' object is not subscriptable