Inheritance
Languages that support object-oriented programming typically include the idea of inheritance to allow for greater code re-use and extensibility, and Python is one of those languages.
To see what we mean by inheritance, let's imagine that we're building a game, "Animal Conserving", that simulates an animal conservatory. We'll be taking care of both fuzzy and ferocious animals in this game, making sure they're well fed and cared for.
We're going to use OOP to organize the data in the game. What do you think the classes should be?
Here's one approach:
# A class for meals
Meal()
# A class for each animal
Panda()
Lion()
Rabbit()
Vulture()
Elephant()
Let's start writing out the class definitions, starting simple with the Meal
class:
class Meal:
def __init__(self, name, kind, calories):
self.name = name
self.kind = kind
self.calories = calories
Here's how we would construct a couple meals:
broccoli = Meal("Broccoli Rabe", "veggies", 20)
bone_marrow = Meal("Bone Marrow", "meat", 100)
Now, some of the animal classes, starting with our long-trunked friend: 🐘
class Elephant:
species_name = "African Savanna Elephant"
scientific_name = "Loxodonta africana"
calories_needed = 8000
def __init__(self, name, age=0):
self.name = name
self.age = age
self.calories_eaten = 0
self.happiness = 0
def eat(self, meal):
self.calories_eaten += meal.calories
print(f"Om nom nom yummy {meal.name}")
if self.calories_eaten > self.calories_needed:
self.happiness -= 1
print("Ugh so full")
def play(self, num_hours):
self.happiness += (num_hours * 4)
print("WHEEE PLAY TIME!")
def interact_with(self, animal2):
self.happiness += 1
print(f"Yay happy fun time with {animal2.name}")
Every elephant shares a few class variables, species_name
, scientific_name
, and calories_needed
. They each have their own name
, age
, calories_eaten
, and happiness
instance variables, however.
Let's make a playful pair of elephants:
el1 = Elephant("Willaby", 5)
el2 = Elephant("Wallaby", 3)
el1.play(2)
el1.interact_with(el2)
Next, let's write a class for our cute fuzzy long-eared friends: 🐇
class Rabbit:
species_name = "European rabbit"
scientific_name = "Oryctolagus cuniculus"
calories_needed = 200
def __init__(self, name, age=0):
self.name = name
self.age = age
self.calories_eaten = 0
self.happiness = 0
def play(self, num_hours):
self.happiness += (num_hours * 10)
print("WHEEE PLAY TIME!")
def eat(self, food):
self.calories_eaten += food.calories
print(f"Om nom nom yummy {food.name}")
if self.calories_eaten > self.calories_needed:
self.happiness -= 1
print("Ugh so full")
def interact_with(self, animal2):
self.happiness += 4
print(f"Yay happy fun time with {animal2.name}")
And construct a few famous rabbits:
rabbit1 = Rabbit("Mister Wabbit", 3)
rabbit2 = Rabbit("Bugs Bunny", 2)
rabbit1.eat(broccoli)
rabbit2.interact_with(rabbit1)
Do you notice similarities between the two animal classes? The structure of the two classes have much in common:
Both
Elephant
andRabbit
have the same three class variable names, though the values of their class variables differ.Elephant
andRabbit
take the same arguments in their__init__
method, and then initialize the same four instance variables. Their__init__
methods are, in fact, identical.The
eat
andplay
methods are identical. Theinteract_with
methods are nearly identical, but vary in how much the happiness instance variable increases.
So it appears that 90% of their code is in fact the same. That violates a popular programming principle, "DRY" (Don't Repeat Yourself), and personally, makes my nose crinkle a little in disgust. Repeated code is generally a bad thing because we need to remember to update that code in multiple places, and we're liable to forget to keep code in sync that's meant to be the same.
Fortunately, we can use inheritance to rewrite this code. Instead of repeating the code, Elephant
and Rabbit
can inherit the code from a base class.
Base class
When multiple classes share similar attributes, you can reduce redundant code by defining a base class and then subclasses can inherit from the base class.
For example, we can first write an Animal
base class, put all the common code in there, and the specific animal species can be subclasses of that base class:
You'll also hear the base class referred to as the superclass.
Here's how we could write the Animal
base class:
class Animal:
species_name = "Animal"
scientific_name = "Animalia"
play_multiplier = 2
interact_increment = 1
def __init__(self, name, age=0):
self.name = name
self.age = age
self.calories_eaten = 0
self.happiness = 0
def play(self, num_hours):
self.happiness += (num_hours * self.play_multiplier)
print("WHEEE PLAY TIME!")
def eat(self, food):
self.calories_eaten += food.calories
print(f"Om nom nom yummy {food.name}")
if self.calories_eaten > self.calories_needed:
self.happiness -= 1
print("Ugh so full")
def interact_with(self, animal2):
self.happiness += self.interact_increment
print(f"Yay happy fun time with {animal2.name}")
We even defined class variables in there. We didn't need to do that, since the values of those variables don't make sense, but it is helpful to show the recommended class variables for the subclasses.
Subclasses
To declare a subclass, put parentheses after the class name and specify the base class in the parentheses:
class Elephant(Animal):
Then the subclasses only need the code that's unique to them. They can redefine any aspect: class variables, method definitions, or constructor. A redefinition is called overriding.
Here's the full Elephant
subclass, which only overrides the class variables:
class Elephant(Animal):
species_name = "African Savanna Elephant"
scientific_name = "Loxodonta africana"
calories_needed = 8000
play_multiplier = 4
interact_increment = 1
num_tusks = 2
Same for the Rabbit
class:
class Rabbit(Animal):
species_name = "European rabbit"
scientific_name = "Oryctolagus cuniculus"
calories_needed = 200
play_multiplier = 10
interact_increment = 2
num_in_litter = 12
Overriding methods
A subclass can also override the methods of the base class. Python will always look for the method definition on the current object's class first, and will only look in the base class if it can't find it there.
We could override interact_with
for pandas, since they're quite solitary creatures:
class Panda(Animal):
species_name = "Giant Panda"
scientific_name = "Ailuropoda melanoleuca"
calories_needed = 6000
def interact_with(self, other):
print(f"I'm a Panda, I'm solitary, go away {other.name}!")
This code will call that overridden method definition instead of the Animal
definition:
panda1 = Panda("Pandeybear", 6)
panda2 = Panda("Spot", 3)
panda1.interact_with(panda2)
The following code would not, however. Do you see why?
pandey = Panda("Pandeybear", 6)
bugs = Rabbit("Bugs Bunny", 2)
bugs.interact_with(pandey)
The object on the left-hand side of the dot notation is of type Rabbit
and there is no interact_with
defined on Rabbit
, so the original Animal
method definition will be used instead.