Understanding python's super
function¶
The super()
function is one of python's least understood (and most misunderstood) features, which is a shame because it can be a very powerful tool if you know how to use it.
In order to clear up any misunderstandings you might have, I'm going to explain super
from the ground up.
First, let's talk about its purpose: super
can be used to access attributes or methods of a parent class from within a child class, for example like this:
class Parent:
def __init__(self):
self.x = 0
class Child(Parent):
def __init__(self):
# call the parent constructor
super().__init__()
self.y = 1
Here we used super
to call the parent class's constructor, like any good child class should do.
But it's not really correct to say that super
lets you access attributes of your parent class. It's a bit more complicated than that. The truth is that super
operates on a class's Method Resolution Order (MRO for short). The MRO is a list of classes that dictates the order in which python searches those classes for attributes and methods. For example, the MRO of Child
looks like this:
>>> Child.__mro__
(<class '__main__.Child'>, <class '__main__.Parent'>, <class 'object'>)
This means that whenever you access an attribute of Child
, python first looks for that attribute in Child
's namespace, then - if it can't find the attribute there - Parent
's namespace, and finally object
's namespace.
super
does the same thing, except it skips some of the classes in the MRO. When we call super()
with no arguments like we did earlier, it's actually a shorthand for super(__class__, self)
(where __class__
is the class the method is defined in). So in the example above, super()
is equivalent to super(Child, self)
. This super(Child, self).__init__()
skips looking for __init__
in Child
's namespace and only starts its search at Parent
. That's where it finds Parent.__init__
and calls it, automagically passing in self
as the first argument so you don't have to.
Now, this is where it gets interesting. You probably assumed that super(Class, instance)
operates on the MRO of Class
, right? Well, you're in for a surprise - it actually operates on the MRO of type(instance)
! Because of the way the MRO works in python, this can lead to some surprises if multiple inheritance is involved.
The MRO and Multiple Inheritance¶
We've already learned that the MRO is a list of classes that dictates how python searches for class attributes and methods. This is easy to understand if the class has only one parent class - in that case the MRO is simply the parent's MRO with the child class inserted at the front:
class Parent:
pass
class Child(Parent):
pass
print(Parent.__mro__)
# (<class '__main__.Parent'>, <class 'object'>)
print(Child.__mro__)
# (<class '__main__.Child'>, <class '__main__.Parent'>, <class 'object'>)
But what if a class has more than one parent? Which parent will come first in the MRO? And what about the parents' parents?
The answer is that python uses an algorithm called C3 linearization to generate the MRO. Here's a short summary of it:
If a class has multiple parents, like
class Child(Parent1, Parent2)
, the order of the parents relative to each other will stay the same in the MRO - i.e.Parent1
will appear beforeParent2
.A child class will appear in the MRO before all of its parent classes.
Here's an example of that:
class Foo:
def __init__(self):
print('foo')
super().__init__()
class Bar:
def __init__(self):
print('bar')
class FooBar(Foo, Bar):
pass
print(FooBar.__mro__)
# (<class '__main__.FooBar'>,
# <class '__main__.Foo'>,
# <class '__main__.Bar'>,
# <class 'object'>)
You can see that FooBar
appears first in the MRO, before its parents. Foo
appears before Bar
, because FooBar
listed Foo
as its first parent and Bar
as its 2nd parent. And last but not least, both Foo
and Bar
appear before object
, because both are subclasses of object
.
Compare this with the MRO of Foo
: Even though Foo
's MRO is (<class '__main__.Foo'>, <class 'object'>)
, in FooBar
's MRO Bar
appears between Foo
and object
! And because super
operates on the MRO of the instance, the super().__init__()
in Foo
will behave differently depending on whether self
is an instance of Foo
or FooBar
:
Foo() # output: foo
FooBar() # output: foo bar
Remember, super(Foo, self)
looks at the MRO of type(self)
and skips everything up to Foo
. When self
is an instance of Foo
, the super().__init__()
in Foo
calls object.__init__
. But when self
is an instance of FooBar
, it calls Bar.__init__
.
That's the cool thing about super
: It can do different things depending on the type of the instance. But you're probably wondering: What is that useful for?
Applications of super
¶
Mixins¶
A mixin is a class that adds features to your class if you inherit from it. Here's an example where we use a mixin to create all kinds of things that can produce sounds:
class NoiseMixin:
def __init__(self, *args, noise, **kwargs):
super().__init__(*args, **kwargs)
self.noise = noise
def make_noise(self):
print(self.noise)
class Animal:
def __init__(self):
self.is_alive = True
class Turtle(Animal):
pass
class Dog(NoiseMixin, Animal):
pass
class Train(NoiseMixin):
pass
lord_voldetort = Turtle()
rex = Dog(noise='bark')
spot = Dog(noise='whimper')
thomas = Train(noise='choo choo')
Without super
we would have a problem implementing NoiseMixin
's __init__
method here: NoiseMixin.__init__
would override any other __init__
, and because of that, instantiating a Dog
would never call Animal.__init__
.
Cooperative multiple inheritance¶
In cooperative multiple inheritance you write a few classes that each implement a small set of features, and then inherit from those classes to create various different combinations of features. It's kind of like a bunch of mixins that all work together. As an example, think of enemy units in a tower defense game. Some units might walk on the ground while others fly. Some units might use ranged weapons while others use melee weapons. In your code, you could write a class for each type of unit and then combine them however you need to:
# abstract base class for all units
class Unit:
def __init__(self, attack, health):
self.attack = attack
self.health = health
# cooperative base classes
class GroundUnit(Unit):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.affected_by_traps = True
class FlyingUnit(Unit):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.affected_by_traps = False
class MeleeUnit(Unit):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ranged = False
class RangedUnit(Unit):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ranged = True
# some actual units
class DwarvenWarrior(GroundUnit, MeleeUnit):
pass
class AngelicArcher(FlyingUnit, RangedUnit):
pass
gimli = DwarvenWarrior(5, 20)
Once again we used super
to chain all our __init__
methods together.