More on inheritance
Using super()
Sometimes we want to be able to call the original method definition from inside the overridden method definition. That is possible using super()
, a function that can delegate method calls to the parent class.
For example, this Lion
class has an eat
method that only calls the original eat
if the meal is meat:
class Lion(Animal):
species_name = "Lion"
scientific_name = "Panthera"
calories_needed = 3000
def eat(self, meal):
if meal.kind == "meat":
super().eat(meal)
Since Lion
inherits from Animal
, that line of code will call the definition of eat
inside Animal
instead, but pass in the Lion
object as the self
.
We'd call it the same way as we usually would:
bones = Meal("Bones", "meat")
mufasa = Lion("Mufasa", 10)
mufasa.eat(bones)
We could have also written this code instead to achieve the same result:
def eat(self, food):
if food.kind == "meat":
Animal.eat(self, food)
However, the great thing about super()
is that it both keeps track of the parent class and takes care of passing in self
. Super convenient!
Using super() with __init__
A very common use of super()
is to call the __init__
. Many times, a subclass wants to initialize all the original instance variables of the base class, but then additionally wants to set a few instance variables specific to its needs.
For example, consider this Elephant
class:
class Elephant(Animal):
species_name = "Elephant"
scientific_name = "Loxodonta"
calories_needed = 8000
def __init__(self, name, age=0):
super().__init__(name, age)
if age < 1:
self.calories_needed = 1000
elif age < 5:
self.calories_needed = 3000
That __init__
method first calls the original __init__
from Animal
and then it conditionally overrides the calories_needed
instance variable in the case of young elephants.
We can create an Elephant
object in the usual manner:
elly = Elephant("Ellie", 3)
print(elly.calories_needed)
Layers of inheritance
Every Python 3 class actually implicitly inherits from the global built-in object
class.
We could have written our Animal
class to explicitly inherit from it, like so:
class Animal(object):
But Python 3 assumes that any class without a base class inherits from object
, so we usually don't bother with the extra code.
The object
class includes a few default methods that we can use on any class, including a default implementation of __init__
that doesn't do much of anything at all - but it does mean that we can write a class without __init__
and won't run into errors when creating new instances.
We can also add more layers of inheritance ourselves, if we realize it would be sensible to have a deeper hierarchy of classes.
To add that new layer of classes, we just define the new classes:
class Herbivore(Animal):
def eat(self, meal):
if meal.kind == "meat":
self.happiness -= 5
else:
super().eat(meal)
class Carnivore(Animal):
def eat(self, meal):
if meal.kind == "meat":
super().eat(meal)
Notice that these subclasses only override a single method, eat
.
Then we change the classes that are currently inheriting from Animal
to instead inherit from either Herbivore
or Carnivore
:
class Rabbit(Herbivore):
class Panda(Herbivore):
class Elephant(Herbivore):
class Vulture(Carnivore):
class Lion(Carnivore):
For a class that represents an omnivorous animal, we could keep its parent class as Animal
and not change it to one of the new subclasses.
Multiple inheritance
This is where things start to get a little wild. A class can actually inherit from multiple other classes in Python.
For example, if Meal
has a subclass of Prey
(since, in the circle of life, animals can become meals!), an animal like Rabbit
could inherit from both Prey
and Herbivore
. Poor rabbits!
Here's the Prey
class:
class Prey(Meal):
kind = "meat"
calories = 200
To inherit from that class as well, we add it to the class names inside the parentheses:
class Rabbit(Herbivore, Prey):
We could even add another subclass to Animal
to represent predators, making it so that interacting with a prey animal instantly turns into mealtime.
class Predator(Animal):
def interact_with(self, other):
if other.kind == "meat":
self.eat(other)
print("om nom nom, I'm a predator")
else:
super().interact_with(other)
We once again would need to modify the class names in the parentheses to make a subclass inherit from Predator
as well:
class Lion(Carnivore, Predator):
Python can look for attributes in any of the parent classes. It first looks in the current class, then looks at the parent class, then looks at that class's parent class, etc.
r = Rabbit("Peter", 4) # Animal __init__
r.play() # Animal method
r.kind # Prey class variable
r.eat(Food("carrot", "veggies")) # Herbivore method
l = Lion("Scar", 12) # Animal __init__
l.eat(Food("zazu", "meat")) # Carnivore method
l.interact_with(r) # Predator method
Actually, the way that it finds attributes is even more complicated than that, since it will also look in sibling classes. We won't dive into that here, but if you're curious, search the web for "Python method resolution order (MRO").
Too much inheritance?
Inheritance is a great way to avoid repeated code and express the relationships between objects in your programs. However, be careful not to over-use it or take it too far. It can lead to confusing code with surprising results when there are multiple places overriding the same method.
A good strategy is to start with as little inheritance as possible, perhaps with a single base class (besides object
), and then only add more levels of inheritance when it's becoming painful not to add them.
Another good idea is to use a "mix-in" approach to multiple inheritance: additional base classes should only add new methods, not override methods. That way, you don't have to worry about the order in which Python will look for methods on the base classes.