Multiple inheritance
Definition
class Car:
def drive(self):
print("wroum")
class GasolineCar(Car):
def drive(self):
print("🌫")
class ElectricCar(Car):
def drive(self):
print("⚡")
class HybridCar(ElectricCar, GasolineCar):
pass
H = HybridCar()
H.drive() # does it call the method drive in GasolineCar or ElectricCar?
⚡
Diamond problem
The problem is that a method not explicitely implemented in HybridCar
should call the method of the same name in some superclass. But if both are implemented in GasolineCar
and ElectricCar
, the system does not how to solve the ambiguity. This problem is called the diamond problem.
from graphviz import Digraph
dot = Digraph(graph_attr=dict(rankdir="BT"), edge_attr={'arrowhead':'empty'})
dot.edges([("GasolineCar", "Car"), ("ElectricCar", "Car"), ("HybridCar", "GasolineCar"), ("HybridCar", "ElectricCar")])
dot
Different solutions
1. Java solution: disallow multiple inheritance
In Java, multiple inheritance is disallowed, except for interfaces.
This example is OK :
interface GazolineCar {
public void drive();
public void fillWithFuel();
}
interface ElectricCar {
public void drive();
public void plug();
}
class HybridCar implements GazolineCar, ElectricCar {
public void drive() {
...
}
public void fillWithFuel() {
...
}
public void plug() {
....
}
}
But in Java, this is disallowed:
public class Car {
public void drive() {
...
}
}
public class GazolineCar extends Car {
public void drive() {
...
}
public void fillWithFuel() {
...
}
}
public class ElectricCar extends Car {
public void drive() {
...
}
public void plug() {
....
}
}
public class HybridCar extends GazolineCar, ElectricCar { //No
...
}
C++ solution: disallowing ambiguities
The C++ compiler disallows ambiguities and provides a compiling error in case of ambiguities.
class Car {
private:
Vector<Wheel> wheel;
};
class GazolineCar: public virtual Car {
public:
void fillWithFuel() {...};
void drive() {...};
};
class ElectricCar: public virtual Car {
public:
void plug() {...};
void drive() {...};
};
class HybridCar: public GazolineCar, public ElectricCar {
public:
void plug() {
}
}
int main()
{
HybridCar* h = new HybridCar();
h->drive();
return 0;
}
Main.cpp:33:8: error: request for member ‘drive’ is ambiguous
33 | h->drive();
| ^~~~~
Main.cpp:18:14: note: candidates are: ‘void ElectricCar::drive()’
18 | void drive() {};
| ^~~~~
Main.cpp:11:14: note: ‘void GazolineCar::drive()’
11 | void drive() {};
| ^~~~~
Python solution: deterministically resolve the ambiguity
In python, we deterministically resolve the ambiguity by computing which method of which class should be called. This is called Method Resolution Order.
Zoom on the Method Resolution Order in Python
The method resolution order (MRO) for class A
is some total order on the superclass of A
. Python searches the first class in the MRO of HybridCar
that implements the method drive
.
Example
from graphviz import Digraph
dot = Digraph(graph_attr=dict(rankdir="BT"), edge_attr={'arrowhead':'empty'})
dot.edges([("GasolineCar", "Car"), ("ElectricCar", "Car"), ("HybridCar", "GasolineCar"), ("HybridCar", "ElectricCar")])
dot
class Car:
def drive(self):
print("wroum")
class GasolineCar(Car):
def drive(self):
print("🌫")
class ElectricCar(Car):
def drive(self):
print("⚡")
class HybridCar(GasolineCar, ElectricCar):
pass
H = HybridCar()
H.drive()
🌫
Here is the MRO is:
- first look in
HybridCar
- then in
GasolineCar
- then in
ElectricCar
- then in
Car
- finally in
object
.
Behind the scene: the C3 linearization algorithm
The C3 linearization algorithm $mro$ takes a class $C$ and computes a total order $mro(C) = (C=C_0, C_1, \dots, C_n = object)$. It satisfies the following properties:
- For any outputs $mro(C) = (C=C_0, C_1, \dots, C_n = object)$, if $C_i$ inherits from $C_j$ then $i < j$
- $mro(C) = (C=C_0, C_1, \dots, C_n = object)$, in case of multiple inheritance
class Cl(..Ci...Cj)
, if $C_i$ is written before $C_j$ then $i < j$ - (Monotonicity) If $i < j$ then $C_i$ is before $C_j$ in $mro(B)$ for any subclass of $B$ of $C$
Here is a pseudo-code for C3 linearization algorithm:
- $mro(object) = [object]$
- if
class C(D)
then $mro(C) = [C] + mro(D)$ - if
class C(D1, ... Dk)
then $mro(C) = [C] + merge(mro(D_1), \dots, mro(D_k))$
The merge operations $merge(L_1, \dots, L_k)$ is:
merge(L1, ..., Lk)
L = empty list
while not all the lists are empty
Find a head C of some of the list L1, ..., Lk
that does not appear in the tail of some list L1, ..., Lk.
If there is not such $C$, raise an error
Remove all the occurrences of C in L1, ..., Lk
L := L + [C]
return L
Get the MRO in Python
It is possible to get the order via the method mro()
.
class Car(): pass
class GasolineCar(Car): pass
class ElectricCar(Car): pass
class HybridCar(GasolineCar, ElectricCar): pass
HybridCar.mro()
[__main__.HybridCar,
__main__.GasolineCar,
__main__.ElectricCar,
__main__.Car,
object]
from graphviz import Digraph
dot = Digraph(graph_attr=dict(rankdir="BT"), edge_attr={'arrowhead':'empty'})
dot.edges([(c, "O") for c in "CABDE"] + [("K1", "C"), ("K1", "A"), ("K1", "B"), ("K3", "A"), ("K3", "D"),
("K2", "B"), ("K2", "D"), ("K2", "E")] + [("Z", k) for k in ["K1", "K3", "K2"]])
dot
class O(): pass
class A(O): pass
class B(O): pass
class C(O): pass
class D(O): pass
class E(O): pass
class K1(C, A, B): pass
class K3(A, D): pass
class K2(B, D, E): pass
class Z(K1, K3, K2): pass
Z.mro()
[__main__.Z,
__main__.K1,
__main__.C,
__main__.K3,
__main__.A,
__main__.K2,
__main__.B,
__main__.D,
__main__.E,
__main__.O,
object]
Inconsistency
Sometimes it is impossible to find a consistent method resolution.
from graphviz import Digraph
dot = Digraph(graph_attr=dict(rankdir="BT"), edge_attr={'arrowhead':'empty'})
dot.edges([("A", "B"), ("A", "C"), ("D", "C"), ("D", "B"), ("E", "A"), ("E", "D")])
dot
class B: pass
class C: pass
class A(B, C): pass
class D(C, B): pass
class E(A, D): pass
E.mro()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[6], line 5
3 class A(B, C): pass
4 class D(C, B): pass
----> 5 class E(A, D): pass
6 E.mro()
TypeError: Cannot create a consistent method resolution
order (MRO) for bases B, C
Stupid example
class Animal:
pass
class TrucBizarre(Animal, type):
pass
Function super
Actually super(cls, x)
returns a wrapper that calls the method as we were in the next class strictly after cls
in the MRO.