Multiple Inheritance: Example


Robot Classes

multiple inheritance with robots

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 and operator overloading.

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 to propagate. 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()
Hi, I am Marvin-Enigma
My health level is: 0.8942125838166225
Hi, I am Enigma-Charles
My health level is: 0.5395101615105286
Hi, I am Marvin-Enigma
My health level is: 0.8767316264276543


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 = 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 + "!")
  

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()
Healing power of Hubert 0.9699954240341351
Healing power of Emma 1

Let's start the healing
Hi, I am Marvin
My health level is: 0.12744529034068042
Marvin has been healed by Emma!
New health level:  0.6813468197617861

Hi, I am Enigma-Alan
My health level is: 0.5855536817845081
Enigma-Alan has been healed by Hubert!
New health level:  0.6367851702576128

Hi, I am Charles-Henry
My health level is: 0.8603258888726255
Charles-Henry is healthy enough!

Hi, I am Marvin-Enigma
My health level is: 0.8942125838166225
Marvin-Enigma is healthy enough!

Hi, I am Enigma-Charles
My health level is: 0.5395101615105286
Enigma-Charles has been healed by Hubert!
New health level:  0.9340518562295808

Hi, I am Marvin-Enigma
My health level is: 0.8767316264276543
Marvin-Enigma is healthy enough!

In interesting question is, what will haven, 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))
Hi, I am Hubert-Emma
My health level is: 0.6398287034544256
<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 consists in 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. This means that 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)
Marvin, Robot 0.6456772614664515
Marvin, Robot 0.25827090458658064
Enigma, Robot 0.9808163365611482
Enigma, Robot 0.3923265346244593
Charles-Henry, Robot 0.793508832842095
Charles-Henry, Robot 0.317403533136838

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)
Before the battle:
Rambo has been healed by Emma!
Rambo, Robot 0.9763083657593981 0.4
Terminator has been healed by Emma!
Terminator, Robot 0.43512977118694296 0.2

After the battle:
Rambo, Robot 0.19526167315187962 0.4
Terminator, Robot 0.1740519084747772 0.2

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)
__init__ of NursingRobot
init of FightingRobot
__init__ of Robot
__init__ of NursingRobot
init of FightingRobot
__init__ of Robot
Donald has been healed by Donald!
Angela has been healed by Angela!
0.3195170033459413 0.6500096816662594
I am the terrible ... Donald
Well, well, everything will be fine ... Angela takes care of you!
0.1374059287870541 0.39918727534046133