Python Overloading And Overriding

In this tutorial, you will master another yet important concept of object-oriented programming which is polymorphism in python. You will walk through what polymorphism is, different ways of implementing polymorphism in programming. Also, you will grasp the concept of overloading and overriding in detail.

What is polymorphism in python?

Polymorphism is one of the basic building blocks of object-oriented programming. To understand it more clearly let's break down the word polymorphism to poly and morphism. In plain English, poly means many and morph means form. So polymorphism means many forms.

Polymorphism in python

Polymorphism in python

In programming, polymorphism can be defined as the process of having multiple forms for a single entity. Python which exclusively works with objects shows polymorphism on objects, which means the objects take multiple forms.

In real life, we humans are polymorphic since we behave differently in different situations. For instance, we behave differently in the office and at home.

What is duck typing in python?

Duck typing is one way of implementing polymorphism that is much related to dynamic typing. Duck typing got its name from the famous saying “if something walks like a duck, quacks like a duck, swims like a duck then it should be a duck”. This implies that something which behaves like a duck then it should be a duck. For instance, if a person walks like a duck and swims like a duck then that person is probably a duck.

Programming languages like Python and Javascript which support dynamic typing widely use the duck typing concept. The unique and common feature of these languages is that they do not declare the variable types explicitly. The compiler won’t raise an error if we assign the same variable with another data type. Observe the following example:

x = 5
print(type(x))
x = 'Learn eTutorial'
print(type(x))

Output:


<class 'int'> 
<class 'str'>

Here we initially assigned variable x with integer value 5 making it a int type. Later we reassign the same variable with a string value ‘Learn eTutorials’ making it an str type. This shows that dynamic typing is applicable on built-in classes like int, str, list, etc.

Similarly, we can implement dynamic typing on custom classes with the help of duck typing.ln duck typing, prominence is given to the presence of  methods and attributes and we do not check for types and classes as they are less significant.

class Duck:
    def walk_swim(self):
        print("I'm a duck, and I can walk and swim.")

class Robot:
     def walk_swim(self):
         print("I'm a Robot, and I can walk and swim.")

class Fish:
     def swim(self):
         print("I'm a fish, and I can swim, but not walk.")
 
for bird in Duck(),Robert(),Fish():
    bird.walk_swim() 

Output:

I'm a duck, and I can walk and swim.
I'm a Robot, and I can walk and swim.
Traceback (most recent call last):
  File "poly_ex.py", line 118, in 
    bird.walk_swim()
AttributeError: 'Fish' object has no attribute 'walk_swim'

In the above code snippet, bird is the object for each class and is testing the presence of walk_swim() method in each class. The Duck class passes the test as it certainly can walk and swim. The same is true for Robot class too since it also implemented the walk_swim() method. But the Fish class ended up in error as it failed to pass the duck testing evaluation as it does not implement the walk_swim() method.

The practical applications of duck typing are iterators, callables, and sorting of len() methods. The key takeaway is that when using duck typing it does not matter which class object we are passing, rather what matters is the object should have the related method in it.

What is operator overloading in python?

Another way of implementing polymorphism in python is through the use of operator overloading. Just recall what an operator is. Any unique symbol that performs some sort of computation is known as an operator. So operator overloading is the process of using the same operator, say +, in multiple forms depending on the operands used.  Check out the below code fragment that shows the changes in behavior of operator ‘+’:

a = 1
b = 2
print(a+b)

c ='Python'
print(c*3)

d = 'Programming'
e = 'Language'
print(d+e) 

Output:

3
PythonPythonPython
ProgrammingLanguage

Here the plus operator performs addition on two integers and at the same time it performs concatenation on two strings.

You may wonder how this is possible and what action actually is taking place behind this operator. Let's take the addition of two integers and break it down internally to understand the process behind these.

Always remember whatever happens in python is solely based on objects. Here  a+b is the expression with two operands and the operands a and b are of type  ‘int’ which internally means a and b are the two attributes of class int.Obviously, the class must have some methods to compute its operands. So when you use a+b, python internally calls the magic method - int.__add__(a,b)method.

a = 1
b = 2
print(int.__add__(a,b))

c ='Python'
print(str.__mul__('Python',3))

d = 'Programming'
e = 'Language'
print(str.__add__('Programming','Language')) 

Output:

3
PythonPythonPython
ProgrammingLanguage

So the moment you use a

  • + operator it calls an obj.__add__()method.
  • - operator calls  an obj.__sub__() method.
  • * operator calls  an obj.__mul__() method.

In python, operator overloading works for built-in classes like int, str, list, etc. However, we can extend the operability of operators to a user-defined class also. This can be accomplished using operator overloading.

class Quantity:
    def __init__(self,w1,w2):
        self.w1=w1
        self.w2=w2

    def __str__(self):
        return "{} ,{}".format(self.w1,self.w2)

q1=Quantity(5,6)
q2=Quantity(7,8)

print(q1)
print(q2)

print(q1+q2)


Output:

(5 ,6)
(7 ,8) 
Traceback (most recent call last):
  File "poly_ex.py", line 51, in 
    print(q1+q2)
TypeError: unsupported operand type(s) for +: 'Quantity' and 'Quantity'

Let's break the example to understand it clearly.

  •  We have defined a class named Quantity with two attributes w1 and w2.
  • __str__() method works the same as the print function. Here this method returns the specified quantities.
  • q1 and q2 are the two instances we created with two different quantities (5,6) and (7,8) respectively.
  • Next, we tried to print the attributes in the instances individually and combinedly.
  • From the example, it is pretty clear that the print(q1) and print(q2) work smoothly but the print(q1+q2) statement raises an error named Type Error. This is because we haven't defined the operation to be performed on the operands and it only works on built-in classes.

So how can we achieve the operability of the operator in a user-defined manner? We just need to define an additional method in the class which makes the + operator workable on the objects. For this, we implemented the magic method __add__() in our class.

class Quantity:
    def __init__(self,w1,w2):
        self.w1=w1
        self.w2=w2

    def __str__(self):
        return "{} ,{}".format(self.w1,self.w2)

    def __add__(self,other):
        w1=self.w1+other.w1
        w2=self.w2+other.w2
        return (w1,w2)

q1=Quantity(5,6)
q2=Quantity(7,8)

print(q1)
print(q2)

print(q1+q2) 

Output:

(5 ,6)
(7 ,8)
(12, 14)

When you called print(q1+q2) python calls the method to add method in the form Quantity.__add__(q1,q2) which is equivalent to q1.__add__(q2).

Magic methods in Python

Magic or Dunder methods are any methods that prepend and append with a double underscore in python. Dunder is short of Double Under. The following table gives you the list of some common magic methods used in python.

OPERATOR EXPRESSION MAGIC METHOD
Addition q1+ q2 ob.__add__(q1,q2)
Subtraction q1 – q2 ob.__sub__(q1,q2)
Multiplication q1 * q2 ob.__mul__(q1,q2)
Division q1 / q2 ob.__truediv__(q1,q2)
Power q1 ** q2 ob.__pow__(q1,q2)
Floor division q1 // q2 ob.__floordiv__(q1,q2)
Modulo operator q1 % q2 ob.__mod__(q1,q2)
Bitwise left shift q1 << q2 ob.__lshift__(q1,q2)
Bitwise right shift q1 >> q2 ob.__rshift__(q1,q2)
Bitwise NOT ~q1 ob.__invert__(q1)
Bitwise AND q1 & q2 ob.__and__(q1,q2)
Bitwise OR q1 | q2 ob.__or__(q1,q2)
Bitwise XOR q1 ^ q2 ob.__xor__(q1,q2)
Less than q1 < q2 ob.__lt__(q1,q2)
Less than equal to q1 <= q2 ob.__le__(q1,q2)
Greater than q1 > q2 ob.__gt__(q1,q2)
Greater than equal to q1 >= q2 ob.__ge__(q1,q2)
Equal to q1 == q2 ob.__eq__(q1,q2)
Not equal to q1 !=  q2 ob.__ne__(q1,q2)

Python method / function overloading

Method overloading, in object-oriented programming, is the ability of a method to behave differently depending on the arguments passed to the method. Method overloading supports compile-time polymorphism.

Clearly saying if you have a class with two methods of the same name and a different number of arguments then the method is said to be overloaded. Observe the below code  fragment:

class A:
    def add(self,a,b):
        s = a+b
        print(s)
    def add(self,a,b,c):
        s = a+b+c
        print(s) 

Here we have a class named A with two methods. Both methods use the same name i.e. add() however the number of arguments in both methods are different. One method has two arguments while the other has three arguments. Here, the method add() is overloaded.

Now let’s check what its output would be while executing in python.

class A:
    def add(self,a,b):
        s = a+b
        print(s)
    def add(self,a,b,c):
        s = a+b+c
        print(s)

ob = A()
ob.add(10,20)
 
Traceback (most recent call last):
  File "poly_ex.py", line 73, in 
    ob.add(10,20)
TypeError: add() missing 1 required positional argument: 'c'

What happens here is that since both methods have the same name python keeps the latest method defined in the class and when the function call is made it checks the latest method, that is, the method with three arguments. The add() with two arguments stays as nothing in the class. This implies that method overloading is not supported by python, unlike other oop languages.

Note: Python does not support method overloading.

However, luckily we have a pythonic way to implement method overloading in python.

class A:
    def add(self,a = None,b = None, c = None):
        if a!=None and b!=None and c!=None:
         print (a+b+c)
        else:
         print(a+b)
    
ob = A()
ob.add(10,20)
ob.add(30,40,50)
ob.add('Programming','Tutorials') 

Output:

30
120
ProgrammingTutorials

The above code snippet displays how method overloading is achieved in python through some tricks. Here we can call the method add() in three different ways.

  • ob.add(10,20)
  • ob.add(30,40,50)
  • ob.add(‘Programming’,’Tutorials’)

Let me remind you again that this is not the ideal way of achieving method overloading and hence is not method overloading in python. But somehow we implemented method overloading in python.

What is method overriding in python?

Overriding, in an object-oriented programming language, is an important concept that supports runtime polymorphism. A method is said to be overridden when a method in child class has the same name, the same number of arguments, and the same return type (signature) as that of a method in its parent class. The key benefit of method overriding is that the child class can provide its own specific implementation to an inherited method without even altering the parent class method.

Hence in method overriding we have two methods, one in parent class which is referred to as overridden method and one is the inherited method in child class which is referred to as an overriding method. Both methods have the same name and same signature.

Method Overriding Example

To make it more understandable, examine the simple example which has two classes : parent class - Mom and child class - Daughter.

#Parent Class 
class Mom:
    def dance(self):
        print("Mom is dancing")
    def cook(self):
        print("Mom is cooking")    

#Child class
class Daughter(Mom):
        pass
   
d = Daughter()
d.dance()
d.cook() 

Output:

Mom is dancing
Mom is cooking

Parent class contains two methods: dance() and cook() which print some messages. For now we kept the daughter class empty without any methods or attributes but we created an object of the Daughter class and used it to call the methods in the Mom. Since the Daughter inherits from Mom it can access the methods in Mom class and produces the output.

Let’s create the child class Daughter which overrides the dance() method.

#parent class
class Mom:
    #overridden method
    def dance(self):
        print("Mom is dancing")
    def cook(self):
        print("Mom is cooking")    

#child class
class Daughter(Mom):
    #overriding method
    def dance(self):
        print("Daughter is dancing ")

d = Daughter()
d.dance()
d.cook() 

Output:

Daughter is dancing
Mom is cooking

Now when we call the object d, the modified version of the dance() method will execute which is clearly reflected in the output. Thus we can conclude that the daughter class overrides the method of Mom class.