Inheritance and Polymorphism in Python

Inheritance is a mechanism which allows us to create a new class - known as child class - that is based upon an existing class - the parent class, by adding new attributes and methods on top of the existing class. When you do so, the child class inherits attributes and methods of the parent class.

Inheritance really shines when you want to create classes that are very similar. All you need to do is to write the code the for the things that they have common in one class - the parent class. And then write code for things that are very specific in a different class - the child class. This saves you from duplicating a lot of code.

Let's take a more concrete example to illustrate this concept.

Suppose we are creating a program which deals with various shapes. Each shape has some common properties. For example, color of the shape, whether it is filled or not and so on. In addition to that, there are some properties which vary from shape to shape. For example, area and perimeter. The area of rectangle is width * length whereas the area of circle is πr² . At first, it might be tempting to create classes for different shapes like this:

class Rectangle:
    def __int__(self, color, filled, width, length):
        self.__color = color
        self.__filled = filled
        self.__width = width
        self.__length = length

    def get_color(self):
        return self.__color

    def set_color(self, color):
        return self.__color = color

    def is_filled(self):
        return self.__filled

    def set_filled(self, filled):
        return self.__filled

    def get_area():
        return self.__width * self.__length

class Circle:
    def __int__(self, color, filled, radius):
        self.__color = color
        self.__filled = filled
        self.__radius = radius        

    def get_color(self):
        return self.__color

    def set_color(self, color):
        return self.__color = color

    def is_filled(self):
        return self.__filled

    def set_filled(self, filled):
        return self.__filled

    def get_area(self):
        return math.pi * self.__radius ** 2

Did you notice that amount of duplicate code we are writing ?

Both classes share same __color and __filled attribute as well as their getter and setter methods. To make the situation worse, If we want to update how any of these methods work, then we would have to visit each classes one by one to make the necessary changes. By using inheritance, we can abstract out common properties to a general Shape class (parent class) and then we can create child classes such as Rectangle, Triangle and Circle that inherits from the Shape class. A child class class inherits all the attributes and methods from it's parent class, but it can also add attributes and methods of it's own.

To create a child class based upon the parent class we use the following syntax:

class ParentClass:
    # body of ParentClass
    # method1 
    # method2

class ChildClass(ParentClass):
    # body of ChildClass
    # method 1
    # method 2

In Object Oriented lingo, When a class c2 inherits from a class c1, we say class c2 extends class c1 or class c2 is derived from class c1.

The following program demonstrate inheritance in action. It creates a class named Shape, which contains attributes and methods common to all shapes, then it creates two child classes Rectangle and Triangle which contains attributes and methods specific to them only.

python101/Chapter-16/inheritance.py

import math

class Shape:

    def __init__(self, color='black', filled=False):
        self.__color = color
        self.__filled = filled

    def get_color(self):
        return self.__color

    def set_color(self, color):
        self.__color = color

    def get_filled(self):
        return self.__filled

    def set_filled(self, filled):
        self.__filled = filled


class Rectangle(Shape):

    def __init__(self, length, breadth):
        super().__init__()
        self.__length = length
        self.__breadth = breadth

    def get_length(self):
        return self.__length

    def set_length(self, length):
        self.__length = length

    def get_breadth(self):
        return self.__breadth

    def set_breadth(self, breadth):
        self.__breadth = breadth

    def get_area(self):
        return self.__length * self.__breadth

    def get_perimeter(self):
        return 2 * (self.__length + self.__breadth)


class Circle(Shape):
    def __init__(self, radius):
        super().__init__()
        self.__radius = radius

    def get_radius(self):
        return self.__radius

    def set_radius(self, radius):
        self.__radius = radius

    def get_area(self):
        return math.pi * self.__radius ** 2

    def get_perimeter(self):
        return 2 * math.pi * self.__radius


r1 = Rectangle(10.5, 2.5)

print("Area of rectangle r1:", r1.get_area())
print("Perimeter of rectangle r1:", r1.get_perimeter())
print("Color of rectangle r1:", r1.get_color())
print("Is rectangle r1 filled ? ", r1.get_filled())
r1.set_filled(True)
print("Is rectangle r1 filled ? ", r1.get_filled())
r1.set_color("orange")
print("Color of rectangle r1:", r1.get_color())

c1 = Circle(12)

print("\nArea of circle c1:", format(c1.get_area(), "0.2f"))
print("Perimeter of circle c1:", format(c1.get_perimeter(), "0.2f"))
print("Color of circle c1:", c1.get_color())
print("Is circle c1 filled ? ", c1.get_filled())
c1.set_filled(True)
print("Is circle c1 filled ? ", c1.get_filled())
c1.set_color("blue")
print("Color of circle c1:", c1.get_color())

Output:

Area of rectagle r1: 26.25
Perimeter of rectagle r1: 26.0
Color of rectagle r1: black
Is rectagle r1 filled ?  False
Is rectagle r1 filled ?  True
Color of rectagle r1: orange

Area of circle c1: 452.39
Perimeter of circle c1: 75.40
Color of circle c1: black
Is circle c1 filled ?  False
Is circle c1 filled ?  True
Color of circle c1: blue

In lines 3-19, we have defined a Shape class. It is a parent class and only contains attributes and methods common to all shapes. This class defines two private attributes __color and __filled, then it provides getter and setter methods for those attributes.

In lines 22-45, we have defined a Rectangle class which inherits from Shape class. Pay close attention to the syntax we are using.

inheritance.png

This lines tells us that Rectangle class extends the Shape class or Rectangle class is a child class of Shape class. Thus the Rectangle class inherits attributes and methods defined in the Shape class. In addition to that, the Rectangle class adds two private attributes, getter and setter methods for private attributes, as well as methods to calculate area and perimeter of the rectangle.

Notice the code in line 25.

super().__init__()

In Python, we use super() function to call the parent class methods. So the above code calls Shape class's __init__() method. This is required to set the values of attributes in the parent class. Otherwise, when you try to access values of attributes defined in parent class using getter or setter methods, you will get an error.

Similarly, In lines 48-63 we have defined a Circle class. Just like Rectangle, it extends the Shape class and adds few attributes and methods of its own.

The code in lines 66-86, creates Rectangle and Circle object and then calls get_area(), get_perimeter(), get_filled(), get_color(), set_color() and set_filled() methods on these objects one by one. Notice how we are able to call methods which are defined in the same class, as well as methods which are defined on the parent class.

Multiple Inheritance #

Python allows us to derive a class from several classes at once, this is known as Multiple Inheritance. Its general format is:

Class ParentClass_1:
    # body of ParentClass_1

Class ParentClass_2:
    # body of ParentClass_2

Class ParentClass_3:
    # body of ParentClass_1

Class ChildClass(ParentClass_1, ParentClass_2, ParentClass_3):
    # body of ChildClass

The ChildClass is derived from three classes ParentClass_1, ParentClass_2, ParentClass_3. As a result, it will inherit attributes and methods from all the three classes.

The following program demonstrates multiple inheritance in action:

python101/Chapter-16/multiple_inheritance.py

class A:
    def explore(self):
        print("explore() method called")

class B:
    def search(self):
        print("search() method called")

class C:
    def discover(self):
        print("discover() method called")

class D(A, B, C):
    def test(self):
        print("test() method called")


d_obj = D()
d_obj.explore()
d_obj.search()
d_obj.discover()
d_obj.test()

Output:

explore() method called
search() method called
discover() method called
test() method called

Polymorphism and Method Overriding #

In literal sense, Polymorphism means the ability to take various forms. In Python, Polymorphism allows us to define methods in the child class with the same name as defined in their parent class.

As we know, a child class inherits all the methods from the parent class. However, you will encounter situations where the method inherited from the parent class doesn't quite fit into the child class. In such cases, you will have to re-implement method in the child class. This process is known as Method Overriding.

In you have overridden a method in child class, then the version of the method will be called based upon the the type of the object used to call it. If a child class object is used to call an overridden method then the child class version of the method is called. On the other hand, if parent class object is used to call an overridden method, then the parent class version of the method is called.

The following program demonstrates method overriding in action:

python101/Chapter-16/method_overriding.py

class A:
    def explore(self):
        print("explore() method from class A")

class B(A):
    def explore(self):
        print("explore() method from class B")


b_obj = B()
a_obj = A()

b_obj.explore()
a_obj.explore()

Output:

explore() method from class B
explore() method from class A

Here b_obj is an object of class B (child class), as a result class B version of the explore() method is called. However, the variable a_obj is an object of class A (parent class), as a result class A version of the explore() method is called.

If for some reason you still want to access overridden method of the parent class in the child class, you can call it using the super() function as follows:

python101/Chapter-16/method_overriding_2.py

class A:
    def explore(self):
        print("explore() method from class A")

class B(A):
    def explore(self):
        super().explore()  # calling the parent class explore() method
        print("explore() method from class B")


b_obj = B()
b_obj.explore()

Output:

explore() method from class A
explore() method from class B

object - The Base Class #

In Python, all classes inherits from the object class implicitly. It means that the following two class definitions are equivalent.

class MyClass:
    pass

class MyClass(object):
    pass

It turns out that the object class provides some special methods with two leading and trailing underscores which are inherited by all the classes. Here are some important methods provided by the object class.

  1. __new__()
  2. __init__()
  3. __str__()

The __new__() method creates the object. After creating the object it calls the __init__() method to initialize attributes of the object. Finally, it returns the newly created object to the calling program. Normally, we don't override __new__() method, however if you want to significantly change the way an object is created, you should definitely override it.

The __str__() method is used to return a nicely formatted string representation of the object. The object class version of __str__() method returns a string containing name of the class and its memory address in hexadecimal. For example:

python101/Chapter-16/__str__method.py

class Jester:
    def laugh(self):
        return print("laugh() called")

obj = Jester()
print(obj)

Output:

<__main__.Jester object at 0x0000000002167E80>

Sure, it is not very helpful. We an easily override this method by defining a method named __str__() in the Jester class as follows.

python101/Chapter-16/overriding__str__method.py

class Jester:
    def laugh(self):
        return "laugh() called"

    def __str__(self):
        return "A more helpful description"

obj = Jester()
print(obj)

Output:

A more helpful description