Inheritance and Polymorphism in Python
Last updated on September 22, 2020
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | class Rectangle:
def __init__(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):
self.__color = color
def is_filled(self):
return self.__filled
def set_filled(self, filled):
self.__filled = filled
def get_area(self):
return self.__width * self.__length
class Circle:
def __init__(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):
self.__color = color
def is_filled(self):
return self.__filled
def set_filled(self, filled):
self.__filled = filled
def get_area(self):
return math.pi * self.__radius ** 2
|
Did you notice the amount of duplicate code we are writing?
Both classes share the 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:
1 2 3 4 5 6 7 8 9 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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.
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:
1 2 3 4 5 6 7 8 9 10 11 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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:
1 2 3 4 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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:
1 2 | 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 the 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
1 2 3 4 5 6 7 8 9 10 11 12 | 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:
1 2 | explore() method from class A
explore() method from class B
|
object - The Base Class #
In Python, all classes inherit from the object
class implicitly. It means that the following two class definitions are equivalent.
1 2 3 4 5 | 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.
__new__()
__init__()
__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 the __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 the __str__()
method returns a string containing the name of the class and its memory address in hexadecimal. For example:
python101/Chapter-16/__str__method.py
1 2 3 4 5 6 | 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
1 2 3 4 5 6 7 8 9 | 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
Load Comments