11. Multiple Inheritance
By Bernd Klein. Last modified: 24 Mar 2024.
Introduction
In the previous chapter of our tutorial, we have covered inheritance, or more specific "single inheritance". As we have seen, a class inherits in this case from one class. Multiple inheritance on the other hand is a feature in which a class can inherit attributes and methods from more than one parent class. The critics point out that multiple inheritance comes along with a high level of complexity and ambiguity in situations such as the diamond problem. We will address this problem later in this chapter.
The widespread prejudice that multiple inheritance is something "dangerous" or "bad" is mostly nourished by programming languages with poorly implemented multiple inheritance mechanisms and above all by improper usage of it. Java doesn't even support multiple inheritance, while C++ supports it. Python has a sophisticated and well-designed approach to multiple inheritance.
A class definition, where a child class SubClassName inherits from the parent classes BaseClass1, BaseClass2, BaseClass3, and so on, looks like this:
class SubclassName(BaseClass1, BaseClass2, BaseClass3, ...): pass
It's clear that all the superclasses BaseClass1, BaseClass2, BaseClass3, ... can inherit from other superclasses as well. What we get is an inheritance tree.
Live Python training
Enjoying this page? We offer live Python training courses covering the content of this site.
Example: CalendarClock
We want to introduce the principles of multiple inheritance with an example. For this purpose, we will implement to independent classes: a "Clock" and a "Calendar" class. After this, we will introduce a class "CalendarClock", which is, as the name implies, a combination of "Clock" and "Calendar". CalendarClock inherits both from "Clock" and "Calendar".
The class Clock simulates the tick-tack of a clock. An instance of this class contains the time, which is stored in the attributes self.hours, self.minutes and self.seconds. Principally, we could have written the __init__
method and the set method like this:
def __init__(self,hours=0, minutes=0, seconds=0): self._hours = hours self.__minutes = minutes self.__seconds = seconds def set(self,hours, minutes, seconds=0): self._hours = hours self.__minutes = minutes self.__seconds = seconds
We decided against this implementation, because we added additional code for checking the plausibility of the time data into the set method. We call the set method from the __init__
method as well, because we want to circumvent redundant code.
The complete Clock class:
"""
The class Clock is used to simulate a clock.
"""
class Clock(object):
def __init__(self, hours, minutes, seconds):
"""
The paramaters hours, minutes and seconds have to be
integers and must satisfy the following equations:
0 <= h < 24
0 <= m < 60
0 <= s < 60
"""
self.set_Clock(hours, minutes, seconds)
def set_Clock(self, hours, minutes, seconds):
"""
The parameters hours, minutes and seconds have to be
integers and must satisfy the following equations:
0 <= h < 24
0 <= m < 60
0 <= s < 60
"""
if type(hours) == int and 0 <= hours and hours < 24:
self._hours = hours
else:
raise TypeError("Hours have to be integers between 0 and 23!")
if type(minutes) == int and 0 <= minutes and minutes < 60:
self.__minutes = minutes
else:
raise TypeError("Minutes have to be integers between 0 and 59!")
if type(seconds) == int and 0 <= seconds and seconds < 60:
self.__seconds = seconds
else:
raise TypeError("Seconds have to be integers between 0 and 59!")
def __str__(self):
return "{0:02d}:{1:02d}:{2:02d}".format(self._hours,
self.__minutes,
self.__seconds)
def tick(self):
"""
This method lets the clock "tick", this means that the
internal time will be advanced by one second.
Examples:
>>> x = Clock(12,59,59)
>>> print(x)
12:59:59
>>> x.tick()
>>> print(x)
13:00:00
>>> x.tick()
>>> print(x)
13:00:01
"""
if self.__seconds == 59:
self.__seconds = 0
if self.__minutes == 59:
self.__minutes = 0
if self._hours == 23:
self._hours = 0
else:
self._hours += 1
else:
self.__minutes += 1
else:
self.__seconds += 1
x = Clock(23,59,59)
print(x)
x.tick()
print(x)
y = str(x)
print(type(y))
OUTPUT:
23:59:59 00:00:00 <class 'str'>
Let's check our exception handling by inputting floats and strings as input. We also check, what happens, if we exceed the limits of the expected values:
try:
x = Clock(24, 45, 17)
except TypeError as e:
print("We caught the exception: ", e)
OUTPUT:
We caught the exception: Hours have to be integers between 0 and 23!
try:
x = Clock(23, 60, 17)
except TypeError as e:
print("We caught the exception: ", e)
OUTPUT:
We caught the exception: Minutes have to be integers between 0 and 59!
We will now create a class "Calendar", which has lots of similarities to the previously defined Clock class. Instead of "tick" we have an "advance" method, which advances the date by one day, whenever it is called. Adding a day to a date is quite tricky. We have to check, if the date is the last day in a month and the number of days in the months vary. As if this isn't bad enough, we have February and the leap year problem.
The rules for calculating a leap year are the following:
- If a year is divisible by 400, it is a leap year.
- If a year is not divisible by 400 but by 100, it is not a leap year.
- A year number which is divisible by 4 but not by 100, it is a leap year.
- All other year numbers are common years, i.e. no leap years.
As a little useful gimmick, we added a possibility to output a date either in British or in American (Canadian) style.
"""
The class Calendar implements a calendar.
"""
class Calendar(object):
months = (31,28,31,30,31,30,31,31,30,31,30,31)
date_style = "British"
@staticmethod
def leapyear(year):
"""
The method leapyear returns True if the parameter year
is a leap year, False otherwise
"""
if not year % 4 == 0:
return False
elif not year % 100 == 0:
return True
elif not year % 400 == 0:
return False
else:
return True
def __init__(self, d, m, y):
"""
d, m, y have to be integer values and year has to be
a four digit year number
"""
self.set_Calendar(d,m,y)
def set_Calendar(self, d, m, y):
"""
d, m, y have to be integer values and year has to be
a four digit year number
"""
if type(d) == int and type(m) == int and type(y) == int:
self.__days = d
self.__months = m
self.__years = y
else:
raise TypeError("d, m, y have to be integers!")
def __str__(self):
if Calendar.date_style == "British":
return "{0:02d}/{1:02d}/{2:4d}".format(self.__days,
self.__months,
self.__years)
else:
# assuming American style
return "{0:02d}/{1:02d}/{2:4d}".format(self.__months,
self.__days,
self.__years)
def advance(self):
"""
This method advances to the next date.
"""
max_days = Calendar.months[self.__months-1]
if self.__months == 2 and Calendar.leapyear(self.__years):
max_days += 1
if self.__days == max_days:
self.__days= 1
if self.__months == 12:
self.__months = 1
self.__years += 1
else:
self.__months += 1
else:
self.__days += 1
x = Calendar(31,12,2012)
print(x, end=" ")
x.advance()
print("after applying advance: ", x)
print("2012 was a leapyear:")
x = Calendar(28,2,2012)
print(x, end=" ")
x.advance()
print("after applying advance: ", x)
x = Calendar(28,2,2013)
print(x, end=" ")
x.advance()
print("after applying advance: ", x)
print("1900 no leapyear: number divisible by 100 but not by 400: ")
x = Calendar(28,2,1900)
print(x, end=" ")
x.advance()
print("after applying advance: ", x)
print("2000 was a leapyear, because number divisibe by 400: ")
x = Calendar(28,2,2000)
print(x, end=" ")
x.advance()
print("after applying advance: ", x)
print("Switching to American date style: ")
Calendar.date_style = "American"
print("after applying advance: ", x)
OUTPUT:
31/12/2012 after applying advance: 01/01/2013 2012 was a leapyear: 28/02/2012 after applying advance: 29/02/2012 28/02/2013 after applying advance: 01/03/2013 1900 no leapyear: number divisible by 100 but not by 400: 28/02/1900 after applying advance: 01/03/1900 2000 was a leapyear, because number divisibe by 400: 28/02/2000 after applying advance: 29/02/2000 Switching to American date style: after applying advance: 02/29/2000
At last, we will introduce our multiple inheritance example. We are now capable of implementing the originally intended class CalendarClock, which will inherit from both Clock and Calendar. The method "tick" of Clock will have to be overridden. However, the new tick method of CalendarClock has to call the tick method of Clock: Clock.tick(self)
"""
Module, which implements the class CalendarClock.
"""
class CalendarClock(Clock, Calendar):
"""
The class CalendarClock implements a clock with integrated
calendar. It's a case of multiple inheritance, as it inherits
both from Clock and Calendar
"""
def __init__(self, day, month, year, hour, minute, second):
Clock.__init__(self,hour, minute, second)
Calendar.__init__(self, day, month, year)
def tick(self):
"""
advance the clock by one second
"""
previous_hour = self._hours
Clock.tick(self)
if (self._hours < previous_hour):
self.advance()
def __str__(self):
return Calendar.__str__(self) + ", " + Clock.__str__(self)
x = CalendarClock(31, 12, 2013, 23, 59, 59)
print("One tick from ",x, end=" ")
x.tick()
print("to ", x)
x = CalendarClock(28, 2, 1900, 23, 59, 59)
print("One tick from ",x, end=" ")
x.tick()
print("to ", x)
x = CalendarClock(28, 2, 2000, 23, 59, 59)
print("One tick from ",x, end=" ")
x.tick()
print("to ", x)
x = CalendarClock(7, 2, 2013, 13, 55, 40)
print("One tick from ",x, end=" ")
x.tick()
print("to ", x)
OUTPUT:
One tick from 12/31/2013, 23:59:59 to 01/01/2014, 00:00:00 One tick from 02/28/1900, 23:59:59 to 03/01/1900, 00:00:00 One tick from 02/28/2000, 23:59:59 to 02/29/2000, 00:00:00 One tick from 02/07/2013, 13:55:40 to 02/07/2013, 13:55:41
The Diamond Problem or the ,,deadly diamond of death''
The "diamond problem" (sometimes referred as the "deadly diamond of death") is the generally used term for an ambiguity that arises when two classes B and C inherit from a superclass A, and another class D inherits from both B and C. If there is a method "m" in A that B or C (or even both of them) has overridden, and furthermore, if it does not override this method, then the question is which version of the method does D inherit? It could be the one from A, B or C.
Let's look at Python. The first Diamond Problem configuration is like this: Both B and C override the method m of A:
class A:
def m(self):
print("m of A called")
class B(A):
def m(self):
print("m of B called")
class C(A):
def m(self):
print("m of C called")
class D(B,C):
pass
If you call the method m on an instance x of D, i.e. x.m(), we will get the output "m of B called". If we transpose the order of the classes in the class header of D in "class D(C,B):", we will get the output "m of C called".
The case in which m will be overridden only in one of the classes B or C, e.g. in C:
class A:
def m(self):
print("m of A called")
class B(A):
pass
class C(A):
def m(self):
print("m of C called")
class D(B,C):
pass
x = D()
x.m()
OUTPUT:
m of C called
Principially, two possibilities are imaginable: "m of C" or "m of A" could be used
We call this script with Python2.7 (python) and with Python3 (python3) to see what's happening:
$ python diamond1.py m of A called $ python3 diamond1.py m of C called
Only for those who are interested in Python version2: To have the same inheritance behaviour in Python2 as in Python3, every class has to inherit from the class "object". Our class A doesn't inherit from object, so we get a so-called old-style class, if we call the script with python2. Multiple inheritance with old-style classes is governed by two rules: depth-first and then left-to-right. If you change the header line of A into "class A(object):", we will have the same behaviour in both Python versions.
Live Python training
Enjoying this page? We offer live Python training courses covering the content of this site.
Upcoming online Courses
super and MRO
We have seen in our previous implementation of the diamond problem, how Python "solves" the problem, i.e. in which order the base classes are browsed through. The order is defined by the so-called "Method Resolution Order" or in short MRO*.
We will extend our previous example, so that every class defines its own method m:
class A:
def m(self):
print("m of A called")
class B(A):
def m(self):
print("m of B called")
class C(A):
def m(self):
print("m of C called")
class D(B,C):
def m(self):
print("m of D called")
Let's apply the method m on an instance of D. We can see that only the code of the method m of D will be executed. We can also explicitly call the methods m of the other classes via the class name, as we demonstrate in the following interactive Python session:
x = D()
B.m(x)
OUTPUT:
m of B called
C.m(x)
OUTPUT:
m of C called
A.m(x)
OUTPUT:
m of A called
Now let's assume that the method m of D should execute the code of m of B, C and A as well, when it is called. We could implement it like this:
class D(B, C):
def m(self):
print("m of D called")
B.m(self)
C.m(self)
A.m(self)
The output is what we have been looking for:
x = D()
x.m()
OUTPUT:
m of D called m of B called m of C called m of A called
But it turns out once more that things are more complicated than they seem. How can we cope with the situation, if both m of B and m of C will have to call m of A as well. In this case, we have to take away the call A.m(self) from m in D. The code might look like this, but there is still a bug lurking in it:
class A:
def m(self):
print("m of A called")
class B(A):
def m(self):
print("m of B called")
A.m(self)
class C(A):
def m(self):
print("m of C called")
A.m(self)
class D(B, C):
def m(self):
print("m of D called")
B.m(self)
C.m(self)
The bug is that the method m of A will be called twice:
x = D()
x.m()
OUTPUT:
m of D called m of B called m of A called m of C called m of A called
One way to solve this problem - admittedly not a Pythonic one - consists in splitting the methods m of B and C in two methods. The first method, called _m
consists of the specific code for B and C and the other method is still called m, but consists now of a call self._m()
and a call A.m(self)
. The code of the method m of D consists now of the specific code of D 'print("m of D called")', and the calls B._m(self)
, C._m(self)
and A.m(self)
:
class A:
def m(self):
print("m of A called")
class B(A):
def _m(self):
print("m of B called")
def m(self):
self._m()
A.m(self)
class C(A):
def _m(self):
print("m of C called")
def m(self):
self._m()
A.m(self)
class D(B, C):
def m(self):
print("m of D called")
B._m(self)
C._m(self)
A.m(self)
Our problem is solved, but - as we have already mentioned - not in a pythonic way:
x = D()
x.m()
OUTPUT:
m of D called m of B called m of C called m of A called
The optimal way to solve the problem, which is the "super" pythonic way, would be calling the super function:
class A:
def m(self):
print("m of A called")
class B(A):
def m(self):
print("m of B called")
super().m()
class C(A):
def m(self):
print("m of C called")
super().m()
class D(B,C):
def m(self):
print("m of D called")
super().m()
It also solves our problem, but in a beautiful design as well:
x = D()
x.m()
OUTPUT:
m of D called m of B called m of C called m of A called
The super function is often used when instances are initialized with the __init__
method:
class A:
def __init__(self):
print("A.__init__")
class B(A):
def __init__(self):
print("B.__init__")
super().__init__()
class C(A):
def __init__(self):
print("C.__init__")
super().__init__()
class D(B,C):
def __init__(self):
print("D.__init__")
super().__init__()
We demonstrate the way of working in the following interactive session:
d = D()
OUTPUT:
D.__init__ B.__init__ C.__init__ A.__init__
c = C()
OUTPUT:
C.__init__ A.__init__
b = B()
OUTPUT:
B.__init__ A.__init__
a = A()
OUTPUT:
A.__init__
The question arises about how the super functions makes decisions. How does it decide which class has to be used? As we have already mentioned, it uses the so-called method resolution order(MRO). It is based on the "C3 superclass linearisation" algorithm. This is called a linearisation, because the tree structure is broken down into a linear order. The mro method can be used to create this list:
D.mro()
OUTPUT:
[__main__.D, __main__.B, __main__.C, __main__.A, object]
B.mro()
OUTPUT:
[__main__.B, __main__.A, object]
A.mro()
OUTPUT:
[__main__.A, object]
Polymorphism
Polymorphism is construed from two Greek words. "Poly" stands for "much" or "many" and "morph" means shape or form. Polymorphism is the state or condition of being polymorphous, or if we use the translations of the components "the ability to be in many shapes or forms. Polymorphism is a term used in many scientific areas. In crystallography it defines the state, if something crystallizes into two or more chemically identical but crystallographically distinct forms. Biologists know polymorphism as the existence of an organism in several form or colour varieties. The Romans even had a god, called Morpheus, who is able to take any human form: Morheus appears in Ovid's metamorphoses and is the son of Somnus, the god of sleep. You can admire Morpheus and Iris in the picture on the right side.
So, before we fall asleep, we get back to Python and to what polymorphism means in the context of programming languages. Polymorphism in Computer Science is the ability to present the same interface for differing underlying forms. We can have in some programming languages polymorphic functions or methods, for example. Polymorphic functions or methods can be applied to arguments of different types, and they can behave differently depending on the type of the arguments to which they are applied. We can also define the same function name with a varying number of parameter.
Let's have a look at the following Python function:
def f(x, y):
print("values: ", x, y)
f(42, 43)
f(42, 43.7)
f(42.3, 43)
f(42.0, 43.9)
OUTPUT:
values: 42 43 values: 42 43.7 values: 42.3 43 values: 42.0 43.9
We can call this function with various types, as demonstrated in the example. In typed programming languages like Java or C++, we would have to overload f to implement the various type combinations.
Our example could be implemented like this in C++:
#includeusing namespace std; void f(int x, int y ) { cout << "values: " << x << ", " << x << endl; } void f(int x, double y ) { cout << "values: " << x << ", " << x << endl; } void f(double x, int y ) { cout << "values: " << x << ", " << x << endl; } void f(double x, double y ) { cout << "values: " << x << ", " << x << endl; } int main() { f(42, 43); f(42, 43.7); f(42.3,43); f(42.0, 43.9); }
Python is implicitly polymorphic. We can apply our previously defined function f even to lists, strings or other types, which can be printed:
def f(x,y):
print("values: ", x, y)
f([3,5,6],(3,5))
OUTPUT:
values: [3, 5, 6] (3, 5)
f("A String", ("A tuple", "with Strings"))
OUTPUT:
values: A String ('A tuple', 'with Strings')
f({2,3,9}, {"a":3.4,"b":7.8, "c":9.04})
OUTPUT:
values: {9, 2, 3} {'a': 3.4, 'b': 7.8, 'c': 9.04}
Live Python training
Enjoying this page? We offer live Python training courses covering the content of this site.
Exercises
Exercise 1
Create a class hierarchy for a hybrid car using multiple inheritance, combining features of both electric and gasoline-powered vehicles.
Tasks:
- Define a base class
ElectricVehicle
with attributes and methods specific to electric vehicles, such as battery capacity and charging time. - Define another base class
GasolineVehicle
with attributes and methods specific to gasoline-powered vehicles, such as fuel tank capacity and fuel efficiency. - Implement a subclass
HybridCar
that inherits from bothElectricVehicle
andGasolineVehicle
, combining features from both parent classes. - Add methods or attributes to the
HybridCar
class to demonstrate how it benefits from both electric and gasoline power sources. - Create instances of the
HybridCar
class and test their functionality.
Solutions
Solution to Exercise 1
class ElectricVehicle:
def __init__(self, battery_capacity, charging_time):
self.battery_capacity = battery_capacity
self.charging_time = charging_time
def charge_battery(self):
print("Charging the battery...")
class GasolineVehicle:
def __init__(self, fuel_tank_capacity, fuel_efficiency):
self.fuel_tank_capacity = fuel_tank_capacity
self.fuel_efficiency = fuel_efficiency
def refuel(self):
print("Refueling the tank...")
class HybridCar(ElectricVehicle, GasolineVehicle):
def __init__(self, battery_capacity, charging_time, fuel_tank_capacity, fuel_efficiency):
ElectricVehicle.__init__(self, battery_capacity, charging_time)
GasolineVehicle.__init__(self, fuel_tank_capacity, fuel_efficiency)
def drive(self):
print("Driving using both electric and gasoline power sources.")
# Creating instances of HybridCar and testing functionality
hybrid_car = HybridCar(battery_capacity=60, charging_time=4, fuel_tank_capacity=40, fuel_efficiency=30)
hybrid_car.charge_battery()
hybrid_car.refuel()
hybrid_car.drive()
OUTPUT:
Charging the battery... Refueling the tank... Driving using both electric and gasoline power sources.
Live Python training
Enjoying this page? We offer live Python training courses covering the content of this site.
Footnotes
- Python has used since 2.3 the ,,C3 superclass linearisation''-algorithm to determine the MRO. -->
Live Python training
Enjoying this page? We offer live Python training courses covering the content of this site.
Upcoming online Courses