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

svg

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

svg

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:

  1. For any outputs $mro(C) = (C=C_0, C_1, \dots, C_n = object)$, if $C_i$ inherits from $C_j$ then $i < j$
  2. $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$
  3. (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

svg

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

svg

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.