Python Polymorphism
The word polymorphism comes from Greek, meaning “many forms“. In Python, polymorphism is an object-oriented concept that allows different objects to respond to the same function or method call in their own unique way.
Types of Polymorphism in Python
Python supports multiple forms of polymorphism, each suited to different use cases, including:
- Duck typing (polymorphism with class methods)
- Method overriding (polymorphism with inheritance)
- Function polymorphism
- Operator overloading
- Polymorphism using abstract base classes
Let’s explore each of these in detail.
(1) Duck Typing (Polymorphism With Class Methods)
Duck typing in Python is a form of polymorphism that focuses on an object’s behavior rather than its type. It follows the idea: “if it walks like a duck and quacks like a duck, it’s a duck”.
In Python, it doesn’t matter what class an object belongs to; what matters is whether the object provides the methods or attributes your code needs.
This makes polymorphism dynamic and flexible, allowing different objects to be used interchangeably.
For example:
class Dog:
def speak(self):
print("Dog barks")
class Cat:
def speak(self):
print("Cat meows")
class Bird:
def speak(self):
print("Bird chirps")
# Common function works with any object that has a 'speak' method
def make_it_speak(animal):
animal.speak()
make_it_speak(Dog()) # Output: Dog barks
make_it_speak(Cat()) # Output: Cat meows
make_it_speak(Bird()) # Output: Bird chirps
In this example, we define a function make_it_speak() that takes any object. It doesn’t care if the object is a Dog, Cat, or Bird. The one requirement is that the object provides a speak() method. As long as the method exists, the function can call it successfully.
(2) Method Overriding (Polymorphism With Inheritance)
Method overriding occurs when a child class provides its own implementation of a method defined in the parent class. This is also called runtime polymorphism because Python determines which method to call during program execution.
For example:
class Animal:
def speak(self):
print("Animal makes a sound")
class Dog(Animal):
# Overrides the parent's speak() method
def speak(self):
print("Dog barks")
class Cat(Animal):
# Overrides the parent's speak() method
def speak(self):
print("Cat meows")
# A list containing different Animal objects (Dog and Cat Instances)
animals = [Dog(), Cat()]
for animal in animals:
# Python dynamically decides which speak() method to call at runtime
animal.speak()
Output:
Dog barks
Cat meows
In this example, we define three classes Animal, Dog, and Cat. Each class has a method named speak() with the same name but with different behavior. A list named animals is created containing objects of these different classes. When the loop iterates through the list and calls speak(), Python automatically executes the version of the method that belongs to the object’s actual class. This demostrates how polymorphism allows different objects to respond to the same method call in their own way.
(3) Function Polymorphism
Python’s built-in functions are polymorphic, meaning they work with multiple data types. For example, len() returns the numbers of characters in a string, the number of elements in a list, and the number of key-value pairs in a dictionary.
For example:
print(len("hello"))
print(len([1, 2, 3, 4]))
print(len({"name": "James", "age": 30}))
Output:
5
4
2
The len() function behaves differently depending on the object type.
(4) Operator Overloading
Operator overloading allows the same operator (such as +, -, *, ==) to have different meanings depending on the data type of the operands, particularly with user defined classes. This is achieved by defining special methods, often called dunder (double underscore) or magic methods, within a class.
Operator Overloading With Built-in Data Types
Python’s built-in data types use operator overloading behind the scenes, allowing operators to automatically perform the appropriate action for each type.
Example: The + Operator
# Integers
print(2 + 3) # Output: 5
# Strings
print("Hello " + "World") # Output: Hello World
# Lists
print([1, 2, 3] + [4, 5, 6]) # Output: [1, 2, 3, 4, 5, 6]
Although the same + operator is used, it behaves differently depending on the data type:
- Adds numbers for integers
- Concatenate strings
- Merge Lists
This is operator overloading in action. Internally, Python calls different methods such as:
- int.__add__()
- str.__add__()
- list.__add__()
So when you write a + b, Python doesn’t directly perform the + operation. Instead, Python secretly converts the expression into a method call like a.__add__(b), allowing each data type to define its own behavior for the + operator.
How operator overloading works in Python
Operator are internally mapped to special methods called dunder methods (short for double underscore).
| Operator | Special Method |
+ | __add__() |
- | __sub__() |
* | __mul__() |
/ | __truediv__() |
== | __eq__() |
< | __lt__() |
> | __gt__() |
Operator Overloading With Custom Classes in Python
Let’s now see how to overload operator for your own objects.
Example: Operator Overloading With a Vector Class
Let’s overload the + operator for a custom Vector class.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
x = self.x + other.x
y = self.y + other.y
return Vector(x, y)
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(2, 3)
v3 = v1 + v2
print(v3) # Output: Vector(3, 5)
This code demostrates operator overloading in Python using a custom Vector class. The class defines an __init__() method that initializes each Vector object with x and y components. The __add__() method overloads the + operator so that when two Vector objects are added together, their corresponding x and y values are summed and the result is returned as a new Vector object. The __str__() method defines how the object is displayed when printed.
Then we create two Vector objects v1 and v2 with values (1, 2) and (2, 3). When v1 + v2 is executed, Python internally calls the __add__() method, producing a new vector with value (3, 5), which is then printed as Vector(3, 5).
(5) Polymorphism Using Abstract Base Classes
Abstract base class in Python defines a strict blueprint of the methods its subclasses must implement, but it cannot be instansiated on its own.
Python provides the abc module to define abstract base classes.
For example:
from abc import ABC, abstractmethod
class Shape(ABC):
"""The Abstract Base Class acting as a blueprint"""
@abstractmethod
def area(self):
pass
@abstractmethod
def draw(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius * self.radius
def draw(self):
return "Drawing the circle"
class Rectange(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def draw(self):
return "Drawing the rectange"
# Creating instances
shapes = [Circle(5), Rectange(2, 3)]
for shape in shapes:
print(shape.draw())
print("Area:", shape.area())
Output:
Drawing the circle
Area: 78.5
Drawing the rectange
Area: 6
In this example, Shape is an abstract base class that defines a common blueprint for all shapes. It declares the area() and draw() methods as abstract, which means any subclass of Shape is required to provide its own implementation of these methods.
The classes Circle and Rectangle inherit from Shape class and therefore must implement both the area() and draw() methods. Each subclass provides behavior specific to its own shape: Circle calculates the area using its radius, while Rectange calculates the area using its width and height. This ensures that all shape objects share a consistent interface while allowing different internal implementations.