python-course.eu

10. Inheritance

By Bernd Klein. Last modified: 24 Mar 2024.

Introduction and Definitions

Inheritance as DNA

No object-oriented programming language would be worthy to look at or use, if it didn't support inheritance. Inheritance was invented in 1969 for Simula. Python not only supports inheritance but multiple inheritance as well. Generally speaking, inheritance is the mechanism of deriving new classes from existing ones. By doing this, we get a hierarchy of classes. In most class-based object-oriented languages, an object created through inheritance (a "child object") acquires all, - though there are exceptions in some programming languages, - of the properties and behaviors of the parent object.

Inheritance allows programmers to create classes that are built upon existing classes, and this enables a class created through inheritance to inherit the attributes and methods of the parent class. This means that inheritance supports code reusability. The methods or generally speaking the software inherited by a subclass is considered to be reused in the subclass. The relationships of objects or classes through inheritance give rise to a directed graph.

The class from which a class inherits is called the parent or superclass. A class which inherits from a superclass is called a subclass, also called heir class or child class. Superclasses are sometimes called ancestors as well. There exists a hierarchical relationship between classes. It's similar to relationships or categorizations that we know from real life. Think about vehicles, for example. Bikes, cars, buses and trucks are vehicles. Pick-ups, vans, sports cars, convertibles and estate cars are all cars and by being cars they are vehicles as well. We could implement a vehicle class in Python, which might have methods like accelerate and brake. Cars, Buses and Trucks and Bikes can be implemented as subclasses which will inherit these methods from vehicle.

Classification of vehicles

Live Python training

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Enrol here

Syntax of Inheritance in Python

The syntax for a subclass definition looks like this:

class DerivedClassName(BaseClassName):
    pass

Instead of the pass statement, there will be methods and attributes like in all other classes. The name BaseClassName must be defined in a scope containing the derived class definition.

Now we are ready for a simple inheritance example with Python code.

Simple Inheritance Example

We will stick with our beloved robots or better Robot class from the previous chapters of our Python tutorial to show how the principle of inheritance works. We will define a class PhysicianRobot, which inherits from Robot.

class Robot:
    
    def __init__(self, name):
        self.name = name
        
    def say_hi(self):
        print("Hi, I am " + self.name)
        
class PhysicianRobot(Robot):
    pass

x = Robot("Marvin")
y = PhysicianRobot("James")

print(x, type(x))
print(y, type(y))

y.say_hi()

OUTPUT:

<__main__.Robot object at 0x7f5c9e46fb10> <class '__main__.Robot'>
<__main__.PhysicianRobot object at 0x7f5c9e48b010> <class '__main__.PhysicianRobot'>
Hi, I am James

Marvin and James

If you look at the code of our PhysicianRobot class, you can see that we haven't defined any attributes or methods in this class. As the class PhysicianRobot is a subclass of Robot, it inherits, in this case, both the method __init__ and say_hi. Inheriting these methods means that we can use them as if they were defined in the PhysicianRobot class. When we create an instance of PhysicianRobot, the __init__ function will also create a name attribute. We can apply the say_hi method to the PhysisicianRobot object y, as we can see in the output from the code above.

Live Python training

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Upcoming online Courses

Enrol here

Difference between type and isinstance

You should also pay attention to the following facts, which we pointed out in other sections of our Python tutorial as well. People frequently ask where the difference between checking the type via the type function or the function isinstance is lies. The difference can be seen in the following code. We see that isinstance returns True if we compare an object either with the class it belongs to or with the superclass. Whereas the equality operator only returns True, if we compare an object with its own class.

x = Robot("Marvin")
y = PhysicianRobot("James")

print(isinstance(x, Robot), isinstance(y, Robot))
print(isinstance(x, PhysicianRobot))
print(isinstance(y, PhysicianRobot))

print(type(y) == Robot, type(y) == PhysicianRobot)

OUTPUT:

True True
False
True
False True

This is even true for arbitrary ancestors of the class in the inheritance line:

class A:
    pass

class B(A):
    pass

class C(B):
    pass

x = C()
print(isinstance(x, A))

OUTPUT:

True

Now it should be clear, why PEP 8, the official Style Guide for Python code, says: "Object type comparisons should always use isinstance() instead of comparing types directly."

Overriding

Let us get back to our new PhysicianRobot class. Imagine now that an instance of a PhysicianRobot should say hi in a different way. In this case, we have to redefine the method say_hi inside of the subclass PhysicianRobot:

class Robot:
    
    def __init__(self, name):
        self.name = name
        
    def say_hi(self):
        print("Hi, I am " + self.name)
        
class PhysicianRobot(Robot):

    def say_hi(self):
        print("Everything will be okay! ") 
        print(self.name + " takes care of you!")

y = PhysicianRobot("James")
y.say_hi()

OUTPUT:

Everything will be okay! 
James takes care of you!

What we have done in the previous example is called overriding. A method of a parent class gets overridden by simply defining a method with the same name in the child class.

If a method is overridden in a class, the original method can still be accessed, but we have to do it by calling the method directly with the class name, i.e. Robot.say_hi(y). We demonstrate this in the following code:

y = PhysicianRobot("Doc James")
y.say_hi()
print("... and now the 'traditional' robot way of saying hi :-)")
Robot.say_hi(y)

OUTPUT:

Everything will be okay! 
Doc James takes care of you!
... and now the 'traditional' robot way of saying hi :-)
Hi, I am Doc James

We have seen that an inherited class can inherit and override methods from the superclass. Besides this a subclass often needs additional methods with additional functionalities, which do not exist in the superclass. An instance of the PhysicianRobot class will need for example the method heal so that the physician can do a proper job. We will also add an attribute health_level to the Robot class, which can take a value between 0 and 1. The robots will 'come to live' with a random value between 0 and 1. If the health_level of a Robot is below 0.8, it will need a doctor. We write a method needs_a_doctor which returns True if the value is below 0.8 and False otherwise. The 'healing' in the heal method is done by setting the health_level to a random value between the old health_level and 1. This value is calculated by the uniform function of the random module.

import random

class Robot:
    
    def __init__(self, name):
        self.name = name
        self.health_level = random.random() 
        
    def say_hi(self):
        print("Hi, I am " + self.name)
        
    def needs_a_doctor(self):
        if self.health_level < 0.8:
            return True
        else:
            return False
        
class PhysicianRobot(Robot):

    def say_hi(self):
        print("Everything will be okay! ") 
        print(self.name + " takes care of you!")


    def heal(self, robo):
        robo.health_level = random.uniform(robo.health_level, 1)
        print(robo.name + " has been healed by " + self.name + "!")

doc = PhysicianRobot("Dr. Frankenstein")        

rob_list = []
for i in range(5):
    x = Robot("Marvin" + str(i))
    if x.needs_a_doctor():
        print("health_level of " + x.name + " before healing: ", x.health_level)
        doc.heal(x)
        print("health_level of " + x.name + " after healing: ", x.health_level)
    rob_list.append((x.name, x.health_level))
    
print(rob_list)

OUTPUT:

health_level of Marvin0 before healing:  0.27804255004922096
Marvin0 has been healed by Dr. Frankenstein!
health_level of Marvin0 after healing:  0.5975612946834138
health_level of Marvin1 before healing:  0.3268433497141947
Marvin1 has been healed by Dr. Frankenstein!
health_level of Marvin1 after healing:  0.4658504587635991
health_level of Marvin2 before healing:  0.3070114314391247
Marvin2 has been healed by Dr. Frankenstein!
health_level of Marvin2 after healing:  0.5060809357227096
health_level of Marvin3 before healing:  0.15192097831635076
Marvin3 has been healed by Dr. Frankenstein!
health_level of Marvin3 after healing:  0.8033835535943196
health_level of Marvin4 before healing:  0.16006553707176496
Marvin4 has been healed by Dr. Frankenstein!
health_level of Marvin4 after healing:  0.6370537193011726
[('Marvin0', 0.5975612946834138), ('Marvin1', 0.4658504587635991), ('Marvin2', 0.5060809357227096), ('Marvin3', 0.8033835535943196), ('Marvin4', 0.6370537193011726)]

When we override a method, we sometimes want to reuse the method of the parent class and at some new stuff. To demonstrate this, we will write a new version of the PhysicianRobot. say_hi should return the text from the Robot class version plus the text " and I am a physician!"

class PhysicianRobot(Robot):

    def say_hi(self):
        Robot.say_hi(self)
        print("and I am a physician!")

        
doc = PhysicianRobot("Dr. Frankenstein")      
doc.say_hi()

OUTPUT:

Hi, I am Dr. Frankenstein
and I am a physician!

We don't want to write redundant code and therefore we called Robot.say_hi(self). We could also use the super function:

class PhysicianRobot(Robot):

    def say_hi(self):
        super().say_hi()
        print("and I am a physician!")

        
doc = PhysicianRobot("Dr. Frankenstein")      
doc.say_hi()

OUTPUT:

Hi, I am Dr. Frankenstein
and I am a physician!

super is not realls necessary in this case. One could argue that it makes the code more maintainable, because we could change the name of the parent class, but this is seldom done anyway in existing classes. The real benefit of super shows when we use it with multiple inheritance.

Live Python training

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Enrol here

Distinction between Overwriting, Overloading and Overriding

Overwriting

If we overwrite a function, the original function will be gone. The function will be redefined. This process has nothing to do with object orientation or inheritance.

def f(x):
    return x + 42

print(f(3))
# f will be overwritten (or redefined) in the following:

def f(x):
    return x + 43
print(f(3))

OUTPUT:

45
46

Overloading

This subchapter will be only interesting for C++ and Java programmers who want to know how overloading can be accomplished in Python. Those who do not know about overloading will not miss it!

In the context of object-oriented programming, you might have heard about "overloading" as well. Even though "overloading" is not directly connected to OOP. Overloading is the ability to define a function with the same name multiple times. The definitions are different concerning the number of parameters and types of the parameters. It's the ability of one function to perform different tasks, depending on the number of parameters or the types of the parameters. We cannot overload functions like this in Python, but it is not necessary either.

This course is, however, not about C++ and we have so far avoided using any C++ code. We want to make an exception now, so that you can see, how overloading works in C++.


#include 
#include 

using namespace std;

int successor(int number) { return number + 1; }

double successor(double number) { return number + 1; }

int main() {

cout << successor(10) << endl;
cout << successor(10.3) << endl;

return 0;

}

We defined the successor function twice: One time for int and the other time with float as a Parameter. In Python the function can be defined like this, as you will know for sure:

def successor(x):
    return x + 1

As x is only a reference to an object, the Python function successor can be called with every object, even though it will create exceptions with many types. But it will work with int and float values!

Having a function with a different number of parameters is another way of function overloading. The following C++ program shows such an example. The function f can be called with either one or two integer arguments:

 #include 
using namespace std;


int f(int n);
int f(int n, int m);

int main() {
    
    cout << "f(3): " << f(3) << endl;
    cout << "f(3, 4): " << f(3, 4) << endl;
    return 0;
}

int f(int n) {
    return n + 42;
}
int f(int n, int m) {
    return n + m + 42; 
}
    

This doesn't work in Python, as we can see in the following example. The second definition of f with two parameters redefines or overrides the first definition with one argument. Overriding means that the first definition is not available anymore.

def f(n):
    return n + 42
 
def f(n,m):
    return n + m + 42

print(f(3, 4))

OUTPUT:

49

If you call f with only one parameter, you will raise an exception:

f(3)

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
 in 
----> 1 f(3)

TypeError: f() missing 1 required positional argument: 'm'


Yet, it is possible to simulate the overloading behaviour of C++ in Python in this case with a default parameter:

def f(n, m=None):
    if m:
        return n + m +42
    else:
        return n + 42

print(f(3), f(1, 3))

OUTPUT:

45 46

The * operator can be used as a more general approach for a family of functions with 1, 2, 3, or even more parameters:

def f(*x):
    if len(x) == 1:
        return x[0] + 42
    elif len(x) == 2:
        return x[0] - x[1] + 5
    else:
        return 2 * x[0] + x[1] + 42

print(f(3), f(1, 2), f(3, 2, 1))

OUTPUT:

45 4 50

Overriding

Overriding is already explained above!

Exercises

Animals in a methematical world

Exercise 1

Create a class hierarchy for different types of animals, including mammals, birds, and reptiles, using inheritance.

Hints:

  1. Define a base class called Animal with common attributes like name, age, and sound.
  2. Implement subclasses for specific animal types such as Mammal, Bird, and Reptile. Each subclass should inherit from the Animal class.
  3. Incorporate additional attributes and methods specific to each animal type. For example, a Mammal class might have attributes like fur_color, number_of_legs, and methods like give_birth and nurse_young.
  4. Use inheritance to create subclasses representing specific species within each animal type. For example, within the Mammal class, create subclasses for Dog, Cat, and Horse.
  5. Implement methods or attributes in the subclasses to demonstrate how inheritance allows for the sharing of attributes and methods from parent classes.
  6. Create instances of the various animal classes and test their functionality to ensure that attributes and methods work as expected.

Exercise 2:

Create a class hierarchy for different types of geometric shapes, including circles, rectangles, and triangles, using inheritance.

Tasks:

  1. Define a base class called Shape with common attributes like color and area.
  2. Implement subclasses for specific shape types such as Circle, Rectangle, and Triangle. Each subclass should inherit from the Shape class.
  3. Incorporate additional attributes and methods specific to each shape type. For example, a Circle class might have attributes like radius and methods like calculate_area.
  4. Use inheritance to create subclasses representing variations within each shape type. For example, within the Rectangle class, create subclasses for Square and Parallelogram.
  5. Implement methods or attributes in the subclasses to demonstrate how inheritance allows for the sharing of attributes and methods from parent classes.
  6. Create instances of the various shape classes and test their functionality to ensure that attributes and methods work as expected.

Live Python training

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Enrol here

Solutions to our Exercises

Solution to Exercise 1

class Animal:
    def __init__(self, name, age, sound):
        """
        Initialize the Animal object with a name, age, and sound.
        """
        self.name = name
        self.age = age
        self.sound = sound

    def make_sound(self):
        """
        Make the sound associated with the animal.
        """
        print(f"{self.name} says: {self.sound}")


class Mammal(Animal):
    def __init__(self, name, age, sound, fur_color, number_of_legs):
        """
        Initialize the Mammal object with additional attributes specific to mammals.
        """
        super().__init__(name, age, sound)
        self.fur_color = fur_color
        self.number_of_legs = number_of_legs

    def give_birth(self, name):
        """
        giving birth to a new mamal
        """
        return Mammal(name, 
                     age=0, 
                     sound=self.sound, 
                     fur_color=self.fur_color, 
                     number_of_legs=self.number_of_legs)

    def nurse_young(self):
        """
        Simulate nursing young (common for mammals).
        """
        print(f"{self.name} nurses its young.")


class Bird(Animal):
    def __init__(self, name, age, sound, wingspan):
        """
        Initialize the Bird object with additional attributes specific to birds.
        """
        super().__init__(name, age, sound)
        self.wingspan = wingspan

    def fly(self):
        """
        Simulate flying (common for birds).
        """
        print(f"{self.name} flies with a wingspan of {self.wingspan}.")


class Reptile(Animal):
    def __init__(self, name, age, sound, scale_color):
        """
        Initialize the Reptile object with additional attributes specific to reptiles.
        """
        super().__init__(name, age, sound)
        self.scale_color = scale_color

    def crawl(self):
        """
        Simulate crawling (common for reptiles).
        """
        print(f"{self.name} crawls with {self.scale_color} scales.")
# Create instances of specific animals
dog = Mammal("Molly", 5, "Woof", "Brown", 4)
eagle = Bird("Eagle", 3, "Screech", "Large")
turtle = Reptile("Turtle", 10, "Hiss", "Green")

# Test methods
dog.make_sound()
baby_dog = dog.give_birth('Charlie')
baby_dog.make_sound()
eagle.make_sound()
eagle.fly()
turtle.make_sound()
turtle.crawl()

OUTPUT:

Molly says: Woof
Charlie says: Woof
Eagle says: Screech
Eagle flies with a wingspan of Large.
Turtle says: Hiss
Turtle crawls with Green scales.

Solution to Exercise 2

import math

class Shape:
    def __init__(self, color):
        """
        Initialize the Shape object with a color.
        """
        self.color = color

    def calculate_area(self):
        """
        Calculate the area of the shape.
        """
        pass  # Placeholder method, to be implemented in subclasses

class Circle(Shape):
    def __init__(self, color, radius):
        """
        Initialize the Circle object with additional attributes specific to circles.
        """
        super().__init__(color)
        self.radius = radius

    def calculate_area(self):
        """
        Calculate the area of the circle.
        """
        return math.pi * self.radius ** 2

class Rectangle(Shape):
    def __init__(self, color, width, height):
        """
        Initialize the Rectangle object with additional attributes specific to rectangles.
        """
        super().__init__(color)
        self.width = width
        self.height = height

    def calculate_area(self):
        """
        Calculate the area of the rectangle.
        """
        return self.width * self.height

class Triangle(Shape):
    def __init__(self, color, base, height):
        """
        Initialize the Triangle object with additional attributes specific to triangles.
        """
        super().__init__(color)
        self.base = base
        self.height = height

    def calculate_area(self):
        """
        Calculate the area of the triangle.
        """
        return 0.5 * self.base * self.height

# Create instances of specific shapes and test their functionality
circle = Circle("Red", 5)
rectangle = Rectangle("Blue", 4, 6)
triangle = Triangle("Green", 3, 4)

print("Circle Area:", circle.calculate_area())       # Output: Circle Area: 78.54
print("Rectangle Area:", rectangle.calculate_area()) # Output: Rectangle Area: 24
print("Triangle Area:", triangle.calculate_area())   # Output: Triangle Area: 6

Live Python training

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Upcoming online Courses

Enrol here