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

svg

from graphviz import Digraph
dot = Digraph(graph_attr=dict(rankdir="BT"), edge_attr={'arrowhead':'empty'})
dot.edges([("Dog", "Animal"), ("Cat", "Animal"), ("Fish", "Animal")])
dot

svg

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

svg

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

svg

from graphviz import Digraph
dot = Digraph(graph_attr=dict(rankdir="BT"), edge_attr={'arrowhead':'empty'})
dot.edges([("ArrayPriorityQueue", "PriorityQueue"), ("BinaryHeap", "PriorityQueue"), ("FibonacciHeap", "PriorityQueue")])
dot

svg

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