Inheritance and super¶
~20 min ยท prerequisite: Encapsulation and properties
What you will be able to do¶
- Create a subclass that reuses the attributes and methods of a parent class.
- Replace a parent's method with your own version, and call the parent's version with
super(). - Read a class's method resolution order with
__mro__and useisinstanceto check a parent relationship.
The idea¶
Inheritance lets you build a new class from an existing one. The new class is called the subclass, and the existing class is the parent class or superclass. The subclass gets every attribute and method of the parent without copying any code. You write only what is different.
Take a bank. A savings account is a basic account that also earns interest. Instead of rewriting the deposit and description code, the savings account inherits it and adds the interest. You write the subclass by putting the parent's name in parentheses, as in class SavingsAccount(BankAccount):.
A subclass can override a method. Overriding means defining a method with the same name, so the subclass version runs when you call it. The parent's version is still there. To reach it, call super(). The expression super() gives you the parent part of the current object, so super().describe() runs the parent's describe. The most common use is in __init__. Calling super().__init__(owner) lets the parent set up its attributes, so the setup code stays in one place.
Python searches for a method in the object's class first, then in its parents. The search order is stored in __mro__, short for method resolution order. The last entry is object, the base of every class.
Use isinstance(savings, BankAccount) to check whether an object belongs to a class or any of its subclasses. The answer is True here, because a savings account is also a bank account.
Inheritance is useful when the subclass really is a kind of the parent. If you only want to reuse a few lines, a plain function or a separate class is often clearer.
Worked example¶
class BankAccount:
def __init__(self, owner):
self.owner = owner
self._balance = 0
def deposit(self, amount):
if amount <= 0:
raise ValueError("deposit must be positive")
self._balance += amount
def describe(self):
return f"{self.owner}: {self._balance}"
class SavingsAccount(BankAccount):
def __init__(self, owner, rate):
super().__init__(owner)
self.rate = rate
def add_interest(self):
self._balance += self._balance * self.rate
def describe(self):
return f"Savings for {super().describe()} at {self.rate}"
savings = SavingsAccount("Ada", 0.05)
savings.deposit(100)
savings.add_interest()
print(savings.describe())
The output is Savings for Ada: 105.0 at 0.05. Line by line, this is what Python does:
class SavingsAccount(BankAccount):creates a subclass. Every method ofBankAccountnow exists onSavingsAccountobjects too.super().__init__(owner)calls the parent's__init__withowner. That setsownerand_balance. The savings account then sets its ownrate.add_interestreads_balance, which the parent's__init__created, and adds 5% of it.describeoverrides the parent's version. It callssuper().describe()to get"Ada: 105.0"and wraps that text.savings.deposit(100)uses the inherited method, andprintshows the overriddendescribe.
Try it¶
The first block checks the inherited and overridden methods. The second block builds a shape hierarchy and checks isinstance and __mro__.
class BankAccount:
def __init__(self, owner):
self.owner = owner
self._balance = 0
def deposit(self, amount):
self._balance += amount
def describe(self):
return f"{self.owner}: {self._balance}"
class SavingsAccount(BankAccount):
def __init__(self, owner, rate):
super().__init__(owner)
self.rate = rate
def describe(self):
return f"Savings for {super().describe()}"
savings = SavingsAccount("Ada", 0.05)
savings.deposit(100)
assert savings.describe() == "Savings for Ada: 100"
assert isinstance(savings, BankAccount)
assert not isinstance(BankAccount("Bo"), SavingsAccount)
assert SavingsAccount.__mro__ == (SavingsAccount, BankAccount, object)
class Shape:
def __init__(self, name):
self.name = name
def describe(self):
return self.name
class Square(Shape):
def __init__(self, side):
super().__init__("square")
self.side = side
def area(self):
return self.side * self.side
sq = Square(4)
assert sq.describe() == "square"
assert sq.area() == 16
assert sq.name == "square"
assert isinstance(sq, Shape)
Common mistakes¶
-
Leaving out
super().__init__. IfSavingsAccount.__init__only setsrate, the parent never runs. Callingdepositthen gives:Call
super().__init__(owner)first in the subclass's__init__. -
Passing
selftosuper(). The callsuper().__init__(self, owner)adds an extra argument, becausesuper()already suppliesself. It gives:Write
super().__init__(owner). -
Naming a parent class that does not exist yet. Writing
class Child(Parent):above the definition ofParentgives:Define the parent class first.
-
Calling a subclass method on a parent object.
BankAccount("Bo").add_interest()gives:Only a
SavingsAccounthasadd_interest. Check the object's class withtype.
Practice¶
- Using the
Shapeclass from the second block, create a subclassRectanglewithwidthandheight. Give it anareamethod, and callsuper().__init__("rectangle")in its__init__. Assert thatRectangle(2, 3).area()is6. - Create a subclass
LoudShapeofShapewhosedescribereturns the parent's text in uppercase, usingsuper().describe(). Assert thatLoudShape("circle").describe()is"CIRCLE". - Create a class
Animalwith a methodspeakthat returns"...". CreateCat(Animal)whosespeakreturns"Meow". Assert thatCat().speak()is"Meow"and thatisinstance(Cat(), Animal)isTrue. - Create a class
Vehiclewhose__init__takes no arguments and setswheelsto2. CreateCar(Vehicle)whose__init__callssuper().__init__()and then setswheelsto4. Assert thatCar().wheelsis4. - Stretch: create
Vehiclewith__init__(self, name)that storesname. CreateCar(Vehicle)with__init__(self, name, wheels), which callssuper().__init__(name)and storeswheels. CreateElectricCar(Car)with__init__(self, name, battery_kwh), which callssuper().__init__(name, 4)and storesbattery_kwh. Assert thatElectricCar("Volt", 60).nameis"Volt", that itswheelsis4, and thatElectricCar.__mro__starts withElectricCar,Car,Vehicle.
Solution
-
```python class Shape: def init(self, name): self.name = name
def describe(self): return self.name
class Rectangle(Shape): def init(self, width, height): super().init("rectangle") self.width = width self.height = height
def area(self):
return self.width * self.height
assert Rectangle(2, 3).area() == 6 ```
-
```python class Shape: def init(self, name): self.name = name
def describe(self): return self.name
class LoudShape(Shape): def describe(self): return super().describe().upper()
assert LoudShape("circle").describe() == "CIRCLE" ```
- ```python class Animal: def speak(self): return "..."
class Cat(Animal): def speak(self): return "Meow"
assert Cat().speak() == "Meow" assert isinstance(Cat(), Animal) ```
- ```python class Vehicle: def init(self): self.wheels = 2
class Car(Vehicle): def init(self): super().init() self.wheels = 4
assert Car().wheels == 4 ```
- ```python class Vehicle: def init(self, name): self.name = name
class Car(Vehicle): def init(self, name, wheels): super().init(name) self.wheels = wheels
class ElectricCar(Car): def init(self, name, battery_kwh): super().init(name, 4) self.battery_kwh = battery_kwh
volt = ElectricCar("Volt", 60) assert volt.name == "Volt" assert volt.wheels == 4 assert ElectricCar.mro[:3] == (ElectricCar, Car, Vehicle) ```
Recap¶
- A subclass inherits the attributes and methods of its parent, and can override a method.
super()gives access to the parent's version of a method, such assuper().__init__(...).isinstancechecks parent relationships, and__mro__shows the order in which Python searches classes.