Python in Detail: Subclassing
Welcome to the Subclassing lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
Classes can inherit from other classes, establishing a parent/child relationship. The child class inherits attributes and methods from its parents, as if we'd defined them directly in the child.
As an example, we might write a
Tigerclass that inherits fromCat. (This isn't ideal, since tigers aren't actually house cats. We'll revisit this issue at the end of this lesson.)>
class Cat:def __init__(self, name):self.name = namedef needed_calories(self):return 300def needs(self):return f"{self.name} needs {self.needed_calories()} calories"- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
keanu = Cat("Keanu")keanu.needs()Result:
'Keanu needs 300 calories'
Tigers share many characteristics with cats. We could duplicate
Cat's code in the newTigerclass, but duplicating code is rarely an ideal solution. Instead,Tigercan inherit fromCatwith the syntaxclass Tiger(Cat). This way,Tigergets all ofCat's methods and attributes, and can also define its own methods and attributes.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
class Tiger(Cat):can_roar = Truebig_keanu = Tiger("Big Keanu") - Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
big_keanu.needed_calories()Result:
300
(A tiger needs to eat more than 300 calories, but we'll deal with that in a moment.)
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
big_keanu.can_roarResult:
True
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
big_keanu.needs()Result:
'Big Keanu needs 300 calories'
We say that
Tigeris a "subclass", "derived class", or "child class" ofCat. Conversely,CatisTiger's "superclass", "base class", or "parent class". These terms are all common, so we'll use them interchangeably in this course.We can decide whether one class is a subclass of another with the built-in
issubclassfunction.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
issubclass(Tiger, Cat)Result:
True
Like
isinstance, we can pass a tuple of classes to ask "is this class a subclass of any of these other classes?"- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
issubclass(Tiger, (int, Cat, str))Result:
True
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
issubclass(Tiger, (int, bool, str))Result:
False
In an earlier lesson, we saw that any instance can override class attributes. The instance attribute shadows the class attribute, meaning that the instance attribute takes precedence.
>
class Cat:favorite_food = "salmon"def __init__(self, name):self.name = namekeanu = Cat("Keanu")keanu.favorite_food = "tuna"keanu.favorite_foodResult:
'tuna'
Subclasses add another detail to how we think about attribute access. When we access an attribute:
- Python checks for an instance attribute.
- Then it checks for an attribute on the instance's class.
- Then it checks the class's superclass.
- Then it checks the class's superclass's superclass, and so on.
Methods in
Tigercan refer to attributes and methods defined inCat. In the next example, the.roarmethod uses thenameattribute andneeded_caloriesmethod fromCat. Neither is defined inTiger!- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
class Tiger(Cat):def roar(self):return f"{self.name} roars that it needs {self.needed_calories()} calories!"big_keanu = Tiger("Big Keanu")big_keanu.roar()Result:
'Big Keanu roars that it needs 300 calories!'
We can also override a method originally defined in a superclass. Below, we update the definition of
needed_caloriesin theTigerclass. That changes the return value of.needs, since that method calls theneeded_caloriesmethod. This works even though.needswas originally defined inCat.(Note that we're calling the original
Cat.needsmethod here, not the newTiger.roarmethod defined above.)>
class Cat:def __init__(self, name):self.name = namedef needed_calories(self):return 300def needs(self):return f"{self.name} needs {self.needed_calories()} calories"class Tiger(Cat):def needed_calories(self):return 6000- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
big_keanu = Tiger("Big Keanu")big_keanu.needs()Result:
'Big Keanu needs 6000 calories'
When we call
.needs, Python dynamically determines which.needed_caloriesmethod to use. The rules are the same as what we showed above for attributes: Python checks the instance, then its class, then the superclass, then the superclass's superclass, and so on. (Ultimately, this happens because Python methods are really just attributes, but that's a topic for a different lesson.)It's often useful for a subclass's method to call the same method in its parent class. In the previous example, we hard-coded the calorie needs for the
Tigerclass to 6,000 calories. We can also determine its needs dynamically by callingCat.needed_calories(self)and modifying the return value.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
class Tiger(Cat):def needed_calories(self):return Cat.needed_calories(self) * 20big_keanu = Tiger("Big Keanu")big_keanu.needs()Result:
'Big Keanu needs 6000 calories'
In that example, control passed back and forth between
TigerandCat:- We called the
Cat.needsmethod onbig_keanu. Cat.needscalledself.needed_calories, which is actuallyTiger.needed_calories.Tiger.needed_caloriescalledCat.needed_calories.
- We called the
Each method call is dynamically dispatched, independent of previous method calls. That's why control flow can go
Cat->Tiger->Cat, as shown above.Now
Tiger's calorie needs are tied toCat's. If we later changeCat.needed_caloriesto return 250, thenTiger.needed_calorieswill return 5,000 (250 * 20).We can also use
super()in place of the parent class' name. For example, inside of a method onTiger,super().needed_calories()callsCat.needed_calories(self).- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
class Tiger(Cat):def needed_calories(self):return super().needed_calories() * 20big_keanu = Tiger("Big Keanu")big_keanu.needed_calories()Result:
6000
Why would we use
super()when we know that the parent class isCat? In this example, they do the same thing, so either one works. But in a future lesson, we'll see multiple inheritance, where a subclass simultaneously inherits from more than one parent class. In that case,super()will make a big difference.All tigers have stripes, but only some have a white coat, so we'll add that as an argument to the constructor. The updated
Tigerclass below takes two arguments:nameandwhite_coat. The name is set in the parent class,Cat, soTigeronly sets thewhite_coatattribute. But the next example has a bug: accessingbig_keanu.namecauses anAttributeError.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
class Tiger(Cat):def __init__(self, name, white_coat):self.white_coat = white_coatbig_keanu = Tiger("Big Keanu", False)big_keanu.nameResult:
AttributeError: 'Tiger' object has no attribute 'name'
In the earlier examples,
Tigerdidn't define.__init__at all, but we could still access the tiger's.name. That worked becauseTigerinherited theCat.__init__method. However, now we're defining a newTiger.__init__method, so Python calls that. The result is thatCat.__init__is never called, so it never gets a chance to setself.name.One possible solution is to set
self.name = nameinside ofTiger.__init__. But that's a bad idea because it duplicates existing functionality inCat. We often say thatCat"owns" thenameattribute, so it's responsible for setting it.Instead,
Tiger.__init__can callCat.__init__. Constructors are just methods, so we can use thesuper()function that we saw earlier:super().__init__(name). Note that we pass along thenameargument, just like we would if we were instantiating a newCat.>
class Cat:def __init__(self, name):self.name = namedef needed_calories(self):return 300def needs(self):return f"{self.name} needs {self.needed_calories()} calories"class Tiger(Cat):def __init__(self, name, white_coat):super().__init__(name)self.white_coat = white_coat- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
big_keanu = Tiger("Big Keanu", False)big_keanu.nameResult:
'Big Keanu'
In real-world code, it's common for the subclass constructor to call the superclass's constructor. When we forget to do that, we often see
AttributeErrors, like the one that we got for.namepreviously.Two final notes about subclassing. First,
superactually takes two arguments: the class and the current instance. In the examples above,super()was a shorthand forsuper(Tiger, self). Modern versions of Python fill those in automatically, so we can callsuper()without any arguments to get the same result. In older Python code, you may seesuper(SomeClass, self).Second, we mentioned that
Tigerinheriting fromCatdoesn't really make sense: tigers aren't house cats. Real-world code often has questionable inheritance relationships like this, but we can make them more correct if we want to. In this case, cats and tigers share a common family, Felidae, but they have different subfamilies, genuses, and species. Here's an inheritance hierarchy modeling cats' and tigers' full scientific classification:>
# All classifications up to the Felidae family that cats and tigers share.class EukaryotaDomain:...class AnimaliaKingdom(EukaryotaDomain):...class ChordataPhylum(AnimaliaKingdom):...class MammaliaClass(ChordataPhylum):...class CarnivoraOrder(MammaliaClass):...class FeliformiaSuborder(CarnivoraOrder):...class FelidaeFamily(FeliformiaSuborder):...# Cat: Felidae -> Felinae -> Felis -> Felis catus ("cat").class FelinaeSubfamily(FelidaeFamily):...class FelisGenus(FelinaeSubfamily):...class FelisCatus(FelisGenus):...# Tiger: Felidae -> Pantherinae -> Panthera -> Panthera tigris ("tiger").class PantherinaeSubfamily(FelidaeFamily):...class PantheraGenus(PantherinaeSubfamily):...class PantheraTigris(PantheraGenus):...That may be more correct, but it's an extreme example of why we often allow imperfections in our real-world inheritance hierarchies. In many cases, the "perfect" hierarchy is cumbersome. It might be easier to model tigers as special house cats, even though that's not actually true.
Here's a code problem:
Cheetahs share some characteristics with house cats. However, cheetahs need to eat seven times as many calories per day.
A
Catclass is already defined below. Define a newCheetahclass that inherits fromCat. Its constructor should take two arguments:nameandis_king. ("King cheetahs" have a rare mutation that results in distinct stripes on their back.)The
Cheetahclass should also have its own.needed_caloriesmethod that overridesCat's. You can calculate a cheetah's calorie requirements by multiplying a cat's.needed_calories()by seven. Be sure to letCatsetnameby callingsuper().__init__(name).class Cat:def __init__(self, name):self.name = nameself._calorie_requirement = 300def needed_calories(self):return self._calorie_requirementdef needs(self):return f"{self.name} needs {self.needed_calories()} calories"class Cheetah(Cat):def __init__(self, name, is_king):super().__init__(name)self.is_king = is_kingdef needed_calories(self):return super().needed_calories() * 7fast_keanu = Cheetah("Fast Keanu", False)assert fast_keanu.needed_calories() == 2100assert fast_keanu.name == "Fast Keanu"assert fast_keanu.is_king == False# Changing a cheetah's `Cat._calorie_requirement` attribute also the cheetah's# needed calories.fast_keanu._calorie_requirement = 280assert fast_keanu.needed_calories() == 1960- Goal:
None
- Yours:
None