Inheritance
Definition
A class A
inherits from B
if all A
are B
.
Examples:
- all cats are animals
- all integers are numbers
- in a video games, all enemies are movable entities
Why inheritance?
- Because we can easily reuse the common shared code for animals for cats. (this is a bit fake)
- It allows abstraction. Some part of the program just consider animals, whether a given object is a cat, dog, etc. is not important. This is called polymorphism.
UML diagrams
We use UML class diagrams that are graphs: nodes are classes and an edge from class A
to B
means "A is a subclass of B". For instance:
from graphviz import Digraph
dot = Digraph(graph_attr=dict(rankdir="BT"), edge_attr={'arrowhead':'empty'})
dot.edges([('Cat', 'Animal')])
dot
from graphviz import Digraph
dot = Digraph(graph_attr=dict(rankdir="BT"), edge_attr={'arrowhead':'empty'})
dot.edges([("Dog", "Animal"), ("Cat", "Animal"), ("Fish", "Animal")])
dot
How to inherit in Python
A subclass of a class is a class that inherits from it. This is abstraction!
- Any cat is an animal.
- Any integer is a number.
class Animal():
lifepoints = 10
def speak(self):
pass
class Cat(Animal):
def speak(self):
print("miaou")
class MagicCat(Cat):
def speak(self):
print("MiAoU")
class Animal():
lifepoints = 10
def speak(self):
pass
class Cat(Animal):
def speak(self):
print("miaou")
class MagicCat(Cat):
def speak(self):
print("MiAoU")
9
10
isinstance
isinstance(Cat(), Cat)
True
garfield = Cat()
isinstance(garfield, Animal)
True
garfield = Cat()
isinstance(garfield, object)
True
isinstance(Cat(), list)
False
isinstance(Cat, Animal)
False
isinstance(Cat, type)
True
isinstance(Cat, object)
True
isinstance(type, object)
True
isinstance(object, type)
True
isinstance(2, type)
False
issubclass
issubclass(Animal, Animal)
True
issubclass(Cat, Animal)
True
issubclass(list, Animal)
False
(Inheritance) Polymorphism
The goal of inheritance is also to treat several objects of different types in the same way. This is called polymorphism. An animal speaks
whatever it is.
class Animal():
lifepoints = 10
class Cat(Animal):
def speak(self):
print("miaou")
class Dog(Animal):
def speak(self):
print("waouf")
class Duck(Animal):
def speak(self):
print("coin")
L = [Cat(), Dog(), Cat(), Cat(), Duck()]
for animal in L:
animal.speak()
miaou
waouf
miaou
miaou
coin
for animal in L:
if animal.type == Cat:
...
elif animal.type == Dog:
...
Duck typing
In Python, actually, we do not need inheritance in that case. As long an object looks like a duck, it is a duck. Here, as long an object can speak
, it is an animal.
class Cat:
def speak(self):
print("miaou")
class Dog:
def speak(self):
print("waouf")
L = [Cat(), Dog(), Cat(), Cat()]
for animal in L:
animal.speak()
miaou
waouf
miaou
miaou
Examples
Example of the number class hierarchy in Python
Here is the class hierarchy for the numbers in Python. See https://peps.python.org/pep-3141/ where you may read the rejected alternatives.
from graphviz import Digraph
dot = Digraph(graph_attr=dict(rankdir="BT"), edge_attr={'arrowhead':'empty'})
dot.edges([('Complex', 'Number'), ('complex', 'Complex'), ('Real', 'Complex'), ('float', 'Real'), ('Rational', 'Real'), ('Integral', 'Rational'), ('int', 'Integral'), ('bool', 'int')])
dot
import numbers
isinstance(5, numbers.Number)
True
Example of datastructures used in Dijkstra's algorithm
Dijkstra's algorithm needs a graph and a priority queue. But the details on how the graph and the priority queue is implemented does not matter for Dijkstra's algorithm. Inheritance enables abstraction. The most obvious way is to declare interfaces:
- an interface
Graph
and then we can for instance implement a grid graph stored as a PNG image file; - an interface
PriorityQueue
and then we could implement for instance a binary heap.
from graphviz import Digraph
dot = Digraph(graph_attr=dict(rankdir="BT"), edge_attr={'arrowhead':'empty'})
dot.edges([("PNGGridGraph", "Graph"), ("VoxelGraph", "Graph"), ("ExplicitGraph", "Graph")])
dot
from graphviz import Digraph
dot = Digraph(graph_attr=dict(rankdir="BT"), edge_attr={'arrowhead':'empty'})
dot.edges([("ArrayPriorityQueue", "PriorityQueue"), ("BinaryHeap", "PriorityQueue"), ("FibonacciHeap", "PriorityQueue")])
dot
In a video game, we could imagine characters that are animals of different types.
Exercice
- Write A* algorithm and adequate classes for representing a graph and a priority queue (e.g. a binary heap).
Overriding
Overriding (spécialisation or redéfinition in french) consists in redefining a method in a subclass.
class Animal:
def speak(self):
print("...")
class Cat(Animal):
def speak(self):
print("miaou")
a = Animal()
a.speak()
c = Cat()
c.speak()
...
miaou
Delegation to superclass methods
Problem
If we override a method, the previous one can a priori not be called anymore.
class Animal:
def __init__(self):
self.number_of_times_I_spoke = 0
def speak(self):
self.number_of_times_I_spoke += 1
class Cat(Animal):
def speak(self):
print("miaou")
a = Animal()
a.speak()
print(a.number_of_times_I_spoke)
c = Cat()
c.speak()
print(c.number_of_times_I_spoke)
1
miaou
0
(Not so great) solution
We can explicitely mention the class name Animal
to have access to the method defined in Animal
.
class Animal:
def __init__(self):
self.number_of_times_I_spoke = 0
def speak(self):
self.number_of_times_I_spoke += 1
class Cat(Animal):
def speak(self):
Animal.speak(self) # yes
print("miaou")
a = Animal()
a.speak()
print(a.number_of_times_I_spoke)
c = Cat()
c.speak()
print(c.number_of_times_I_spoke)
1
miaou
1
Example with the initializer
class Animal:
def __init__(self):
self.health = 3
class Cat(Animal):
def __init__(self):
self.mustache = True
garfield = Cat()
garfield.health
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[4], line 10
7 self.mustache = True
9 garfield = Cat()
---> 10 garfield.health
AttributeError: 'Cat' object has no attribute 'health'
The solution is again to explicitely mention the class name Animal
to have access to the method defined in Animal
.
class Animal:
def __init__(self):
self.health = 3
class Cat(Animal):
def __init__(self):
Animal.__init__(self) # yes
self.mustache = True
garfield = Cat()
garfield.health
3
The super
function
Problem
Writing Animal.speak(self)
is not so robust. It may fail if we add a new subclass etc.
Solution
The function super
creates a wrapper object of self
for accessing methods as we were in the next superclass of the current one.
class Animal:
def __init__(self):
self.number_of_times_I_spoke = 0
def speak(self):
self.number_of_times_I_spoke += 1
class Cat(Animal):
def speak(self):
super().speak() # yes
print("miaou")
a = Animal()
a.speak()
print(a.number_of_times_I_spoke)
c = Cat()
c.speak()
print(c.number_of_times_I_spoke)
1
miaou
1
class Animal:
def __init__(self):
self.health = 3
class Cat(Animal):
def __init__(self):
super().__init__()
self.mustache = True
garfield = Cat()
garfield.health
10
What is exactly this super()
?
Actually, super()
is syntaxic sugar for super(Cat, self)
. It gives a wrapper object of self
which calls the methods as if we were in the next superclass of Cat
with the very instance.
class Animal:
def __init__(self):
self.health = 3
class Cat(Animal):
def __init__(self):
super(Animal, self).__init__()
self.mustache = True
garfield = Cat()
garfield.health
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[9], line 11
8 self.mustache = True
10 garfield = Cat()
---> 11 garfield.health
AttributeError: 'Cat' object has no attribute 'health'
More precisely, it is syntactic sugar for super(self.__class__, self)
where __class__
is a magic method that returns the class of an object.
Quiz
class Animal:
def speakTwice(self):
self.speak()
self.speak()
def speak(self):
pass
class Cat(Animal):
def speak(self):
print("miaou")
class Dog(Animal):
def speak(self):
print("waouf")
for a in [Cat(), Dog()]:
a.speakTwice()
miaou
miaou
waouf
waouf
class Animal:
def speakTwice(self):
self.speak()
self.speak()
def speak(self):
print("no sound")
class Cat(Animal):
def speak(self):
print("miaou")
class Dog(Animal):
def speak(self):
print("waouf")
for a in [Cat(), Dog()]:
a.speakTwice()
miaou
miaou
waouf
waouf
class Animal:
def speakTwice(self):
self.speak()
self.speak()
def speak(self):
print("no sound")
class Cat(Animal):
def speak(self):
print("miaou")
class Dog(Animal):
def speak(self):
super().speak()
print("waouf")
for a in [Cat(), Dog()]:
a.speakTwice()
miaou
miaou
no sound
waouf
no sound
waouf
class Animal:
def __init__(self):
self.sound = "no sound"
def speakTwice(self):
self.speak()
self.speak()
def speak(self):
print(self.sound)
class Cat(Animal):
def __init__(self):
self.sound = "miaou"
class Dog(Animal):
def __init__(self):
self.sound = "waouf"
def speak(self):
super(Dog, self).speak()
print("tss tss")
for a in [Cat(), Dog()]:
a.speakTwice()
miaou
miaou
waouf
tss tss
waouf
tss tss
class Animal:
def speakTwice(self):
self.speak()
self.speak()
def speak(self):
print("no sound")
class Cat(Animal):
def speak(self):
print("miaou")
class Dog(Animal):
def speak(self):
print("waouf")
for a in [Cat(), Dog()]:
super(Cat, a).speak()
no sound
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[16], line 18
15 print("waouf")
17 for a in [Cat(), Dog()]:
---> 18 super(Cat, a).speak()
TypeError: super(type, obj): obj must be an instance or subtype of type
class Animal:
def speakTwice(self):
self.speak()
self.speak()
def speak(self):
print("no sound")
class Cat(Animal):
def speak(self):
print("miaou")
class Dog(Animal):
def speak(self):
print("waouf")
for a in [Cat(), Cat()]:
super(Cat, a).speak()
no sound
no sound
class Animal:
def speakTwice(self):
print(type(self))
self.speak()
self.speak()
def speak(self):
print("no sound")
class Cat(Animal):
def speak(self):
print("miaou")
class Dog(Animal):
def speak(self):
print("waouf")
for a in [Cat(), Cat()]:
super(Cat, a).speakTwice()
<class '__main__.Cat'>
miaou
miaou
<class '__main__.Cat'>
miaou
miaou