12. Multiple Inheritance: Example
By Bernd Klein. Last modified: 24 Mar 2024.
Robot Classes
This chapter of our tutorial is meant to deepen the understanding of multiple inheritance that the reader has built up in our previous chapter. We will provide a further extentive example for this important object oriented principle of the programming language Python. We will use a variation of our Robot class as the superclass. We will also summarize some other important aspects of object orientation with Python like properties. We will also work out the differences between overwriting, overloading and overriding.
This example has grown during my onsite Python training classes, because I urgently needed simple and easy to understand examples of subclassing and above all one for multiple inheritance.
Starting from the superclass Robot
we will derive two classes: A FightingRobot
class and a NursingRobot
class.
Finally we will define a 'combination' of both the FightingRobot
class and the NursingRobot
class, i.e. we will implement a class FightingNurseRobot
, which will inherit both from FightingRobot
and NursingRobot
.
Let us start with our Robot
class:
We use a private class attribute __illegal_names
containing a set of names not allowed to be used for naming robots.
By providing an __add__
method we make sure that our robots are capable of propagating. The name of the resulting robot will be automatically created. The name of a 'baby' robot will consist of the concatenation of the names of both parents separated by an hyphen. If a parent name has a name containing a hyphen, we will use only the first part before the hyphen.
The robots will 'come to live' with a random value between 0 and 1 for the attribute health_level
. If the health_level of a Robot is below a threshold, which is defined by the class attribute Robot.__crucial_health_level
, it will need the nursing powers of a robot from the NursingClass
. To determine if a Robots needs healing, we provide a method needs_a_nurse
which returns True if the value is below Robot.__crucial_health_level
and False otherwise.
import random
class Robot():
__illegal_names = {"Henry", "Oscar"}
__crucial_health_level = 0.6
def __init__(self, name):
self.name = name #---> property setter
self.health_level = random.random()
@property
def name(self):
return self.__name
@name.setter
def name(self, name):
if name in Robot.__illegal_names:
self.__name = "Marvin"
else:
self.__name = name
def __str__(self):
return self.name + ", Robot"
def __add__(self, other):
first = self.name.split("-")[0]
second = other.name.split("-")[0]
return Robot(first + "-" + second)
def needs_a_nurse(self):
if self.health_level < Robot.__crucial_health_level:
return True
else:
return False
def say_hi(self):
print("Hi, I am " + self.name)
print("My health level is: " + str(self.health_level))
We can test the newly designed Robot
class now. Watch out how the hyphened names change from generation to generation:
first_generation = (Robot("Marvin"),
Robot("Enigma-Alan"),
Robot("Charles-Henry"))
gen1 = first_generation # used as an abbreviation
babies = [gen1[0] + gen1[1], gen1[1] + gen1[2]]
babies.append(babies[0] + babies[1])
for baby in babies:
baby.say_hi()
OUTPUT:
Hi, I am Marvin-Enigma My health level is: 0.8046874938358127 Hi, I am Enigma-Charles My health level is: 0.9068055696667927 Hi, I am Marvin-Enigma My health level is: 0.9452000668967779
Live Python training
Enjoying this page? We offer live Python training courses covering the content of this site.
Subclass NursingRobot
We are ready now for subclassing the Robot
class. We will start by creating the NursingRobot
class. We extend the __init__
method with a new attribute healing_power
. At first we have to understand the concept of 'healing power'. Generally, it makes only sense to heal a Robot
, if its health level is below 1. The 'healing' in the heal method is done by setting the health_level to a random value between the old health_level and healing_power
of a ǸursingRobot
. This value is calculated by the uniform function of the random module.
class NursingRobot(Robot):
def __init__(self, name="Hubert", healing_power=None):
super().__init__(name)
if healing_power is None:
self.healing_power = random.uniform(0.8, 1)
else:
self.healing_power = healing_power
def say_hi(self):
print("Well, well, everything will be fine ... " + self.name + " takes care of you!")
def say_hi_to_doc(self):
Robot.say_hi(self)
def heal(self, robo):
if robo.health_level > self.healing_power:
print(self.name + " not strong enough to heal " + robo.name)
else:
robo.health_level = random.uniform(robo.health_level, self.healing_power)
print(robo.name + " has been healed by " + self.name + "!")
Let's heal the robot class instances which we created so far. If you look at the code, you may wonder about the function chain
, which is a generator from the itertools
module. Logically, the same thing happens, as if we had used first_generation + babies
, but chain
is not creating a new list. chain
is iterating over both lists, one after the other and this is efficient!
from itertools import chain
nurses = [NursingRobot("Hubert"),
NursingRobot("Emma", healing_power=1)]
for nurse in nurses:
print("Healing power of " + nurse.name,
nurse.healing_power)
print("\nLet's start the healing")
for robo in chain(first_generation, babies):
robo.say_hi()
if robo.needs_a_nurse():
# choose randomly a nurse:
nurse = random.choice(nurses)
nurse.heal(robo)
print("New health level: ", robo.health_level)
else:
print(robo.name + " is healthy enough!")
print()
OUTPUT:
Healing power of Hubert 0.8505907920868895 Healing power of Emma 1 Let's start the healing Hi, I am Marvin My health level is: 0.2729220195471842 Marvin has been healed by Hubert! New health level: 0.49873537992710615 Hi, I am Enigma-Alan My health level is: 0.5781206628493454 Enigma-Alan has been healed by Emma! New health level: 0.7543798686704403 Hi, I am Charles-Henry My health level is: 0.04402336625130754 Charles-Henry has been healed by Hubert! New health level: 0.5346093674217587 Hi, I am Marvin-Enigma My health level is: 0.8046874938358127 Marvin-Enigma is healthy enough! Hi, I am Enigma-Charles My health level is: 0.9068055696667927 Enigma-Charles is healthy enough! Hi, I am Marvin-Enigma My health level is: 0.9452000668967779 Marvin-Enigma is healthy enough!
An interesting question is, what would happen, if Hubert and Emma get added. The question is, what the resulting type will be:
x = nurses[0] + nurses[1]
x.say_hi()
print(type(x))
OUTPUT:
Hi, I am Hubert-Emma My health level is: 0.37433207234817967 <class '__main__.Robot'>
We see that the result of addition of two nursing robots is a plain robot of type Robot
. This is not wrong but bad design. We want the resulting robots to be an instance of the NursingRobot
class of course. One way to fix this would be to overload the __add__
method inside of the NursingRobot
class:
def __add__(self, other):
first = self.name.split("-")[0]
second = other.name.split("-")[0]
return NursingRobot(first + "-" + second)
This is also bad design, because it is mainly a copy the original function with the only exception of creating an instance of NursingRobot instead of a Robot instance.
An elegant solution would be having __add__
more generally defined. Instead of creating always a Robot
instance, we could have made it dependent on the type of self by using type(self)
. For simplicity's sake we repeat the complete example:
import random
class Robot():
__illegal_names = {"Henry", "Oscar"}
__crucial_health_level = 0.6
def __init__(self, name):
self.name = name #---> property setter
self.health_level = random.random()
@property
def name(self):
return self.__name
@name.setter
def name(self, name):
if name in Robot.__illegal_names:
self.__name = "Marvin"
else:
self.__name = name
def __str__(self):
return self.name + ", Robot"
def __add__(self, other):
first = self.name.split("-")[0]
second = other.name.split("-")[0]
return type(self)(first + "-" + second)
def needs_a_nurse(self):
if self.health_level < Robot.__crucial_health_level:
return True
else:
return False
def say_hi(self):
print("Hi, I am " + self.name)
print("My health level is: " + str(self.health_level))
class NursingRobot(Robot):
def __init__(self, name="Hubert", healing_power=None):
super().__init__(name)
if healing_power:
self.healing_power = healing_power
else:
self.healing_power = random.uniform(0.8, 1)
def say_hi(self):
print("Well, well, everything will be fine ... " + self.name + " takes care of you!")
def say_hi_to_doc(self):
Robot.say_hi(self)
def heal(self, robo):
if robo.health_level > self.healing_power:
print(self.name + " not strong enough to heal " + robo.name)
else:
robo.health_level = random.uniform(robo.health_level, self.healing_power)
print(robo.name + " has been healed by " + self.name + "!")
Subclass FightingRobot
Unfortunately, our virtual robot world is not better than their human counterpart. In other words, there will be some fighting going on as well. We subclass Robot
once again to create a class with the name FightingRobot
.
class FightingRobot(Robot):
__maximum_damage = 0.2
def __init__(self, name="Hubert",
fighting_power=None):
super().__init__(name)
if fighting_power:
self.fighting_power = fighting_power
else:
max_dam = FightingRobot.__maximum_damage
self.fighting_power = random.uniform(max_dam, 1)
def say_hi(self):
print("I am the terrible ... " + self.name)
def attack(self, other):
other.health_level = \
other.health_level * self.fighting_power
if isinstance(other, FightingRobot):
# the other robot fights back
self.health_level = \
self.health_level * other.fighting_power
Let us see now, how the fighting works:
fighters = (FightingRobot("Rambo", 0.4),
FightingRobot("Terminator", 0.2))
for robo in first_generation:
print(robo, robo.health_level)
fighters[0].attack(robo)
print(robo, robo.health_level)
OUTPUT:
Marvin, Robot 0.49873537992710615 Marvin, Robot 0.19949415197084247 Enigma-Alan, Robot 0.7543798686704403 Enigma-Alan, Robot 0.30175194746817613 Charles-Henry, Robot 0.5346093674217587 Charles-Henry, Robot 0.21384374696870348
What about Rambo fighting Terminator? This spectacular fight can be viewed in the following code:
# let us make them healthier first:
print("Before the battle:")
for fighter in fighters:
nurses[1].heal(fighter)
print(fighter,
fighter.health_level,
fighter.fighting_power)
fighters[0].attack(fighters[1])
print("\nAfter the battle:")
for fighter in fighters:
print(fighter,
fighter.health_level,
fighter.fighting_power)
OUTPUT:
Before the battle: Rambo has been healed by Emma! Rambo, Robot 0.807864589918383 0.4 Terminator has been healed by Emma! Terminator, Robot 0.7582930215235727 0.2 After the battle: Rambo, Robot 0.1615729179836766 0.4 Terminator, Robot 0.3033172086094291 0.2
Live Python training
Enjoying this page? We offer live Python training courses covering the content of this site.
Upcoming online Courses
An Example of Multiple Inheritance
The underlying idea of the following class FightingNurseRobot
consists in having robots who can both heal and fight.
class FightingNurseRobot(NursingRobot, FightingRobot):
def __init__(self, name, mode="nursing"):
super().__init__(name)
self.mode = mode # alternatively "fighting"
def say_hi(self):
if self.mode == "fighting":
FightingRobot.say_hi(self)
elif self.mode == "nursing":
NursingRobot.say_hi(self)
else:
Robot.say_hi(self)
We will instantiate two instances of FightingNurseRobot
. You can see that after creation they are capable to heal themselves if necessary. They can also attack other robots.
fn1 = FightingNurseRobot("Donald", mode="fighting")
fn2 = FightingNurseRobot("Angela")
if fn1.needs_a_nurse():
fn1.heal(fn1)
if fn2.needs_a_nurse():
fn2.heal(fn2)
print(fn1.health_level, fn2.health_level)
fn1.say_hi()
fn2.say_hi()
fn1.attack(fn2)
print(fn1.health_level, fn2.health_level)
OUTPUT:
0.8892683470807023 0.8699751643457001 I am the terrible ... Donald Well, well, everything will be fine ... Angela takes care of you! 0.5086139994871022 0.23667056868943398
Proper way by Using **kw
import random
class Robot:
def __init__(self, name, health_level, **kwargs):
print("in Robot.__init__", kwargs)
self.health_level = health_level
self.name = name
class HealingRobot(Robot):
def __init__(self, healing_power, **kwargs):
print("in HealingRobot.__init__", kwargs)
super().__init__(**kwargs) # FightingRobot, if self is a FightingHealingRobot
self.healing_power = healing_power
def say_hi(self):
super().say_hi()
print("and I am a healer!")
def heal(self, robo):
robo.health_level = random.uniform(robo.health_level, 1)
print(robo.name + " has been healed by " + self.name + "!")
class FightingRobot(Robot):
def __init__(self, fighting_power=1, **kwargs):
print("in FightingRobot.__init__", kwargs)
super().__init__(**kwargs)
self.fighting_power = fighting_power
def say_hi(self):
super().say_hi()
print("and I a gruesome fighter!")
def attack(self, robo):
robo.health_level = random.uniform(0, robo.health_level)
print(robo.name + " has been attacked by " + self.name + "!")
class FightingHealingRobot(HealingRobot, FightingRobot):
def __init__(self,
name,
health_level,
healing_power,
fighting_power,
mode='healing',
**kw):
print("in FightingHealingRobot.__init__", **kw)
super().__init__(name=name,
health_level=health_level,
healing_power=healing_power,
fighting_power=fighting_power,
**kw)
def say_hi(self):
if self.mode == "fighting":
FightingRobot.say_hi(self)
elif self.mode == "healing":
HealingRobot.say_hi(self)
else:
Robot.say_hi(self)
x = FightingHealingRobot(name='Rambo',
health_level=0.9,
fighting_power=0.7,
healing_power=0.9)
print(x.__dict__)
OUTPUT:
in FightingNurseRobot.__init__ in NursingRobot.__init__ {'name': 'Rambo', 'health_level': 0.9, 'fighting_power': 0.7} in FightingRobot.__init__ {'name': 'Rambo', 'health_level': 0.9} in Robot.__init__ {} {'health_level': 0.9, 'name': 'Rambo', 'fighting_power': 0.7, 'healing_power': 0.9}
**Explanation of **kw
usage:**
-
Flexibility:
**kw
allows the class to accept any number of additional keyword arguments without explicitly defining them in the method signature.- This provides flexibility and allows users of the class to pass any custom arguments they may need without modifying the class definition.
-
Passing to Superclass:
- When calling the superclass constructor using
super().__init__()
, any additional keyword arguments that are not explicitly consumed by the subclass need to be passed up to the superclass. - Using
**kw
ensures that any such additional arguments are correctly forwarded to the superclass constructor.
- When calling the superclass constructor using
-
Forwarding Arguments:
- It's common practice to use
**kw
to collect any unspecified keyword arguments and pass them to another function or method. - This allows for cleaner and more concise code, especially when dealing with functions or methods that accept variable arguments.
- It's common practice to use
Live Python training
Enjoying this page? We offer live Python training courses covering the content of this site.
Upcoming online Courses