Polymorphism is a core concept in object-oriented programming that allows objects of different classes to be used interchangeably if they share a common interface. It provides a way to create flexible and reusable code that can work with objects of various types.
In Python, polymorphism is supported through abstract base classes and interfaces. Python programmers leverage polymorphism to write general functions and methods that can handle objects from various subclasses or implementations. This allows for more dynamic and extendable code.
This comprehensive guide will explain polymorphism in detail with a focus on using different classes through a common interface in Python. We will cover key topics including:
Table of Contents
Open Table of Contents
- What is Polymorphism?
- Enabling Polymorphism in Python
- Abstract Base Classes
- ABC Syntax
- ABC Example - Graphical Shape Classes
- The
collections.abc
Module - Interfaces in Python
- Method Overriding
- Duck Typing
- When to Use Polymorphism
- Polymorphism vs Overloading vs Overriding
- Practical Example - Game Characters
- Conclusion
What is Polymorphism?
Polymorphism stems from the Greek words poly
meaning many and morph
meaning form. It refers to the ability of different object classes to implement the same interface or method signature.
In simple terms, polymorphism allows objects with different underlying forms to be treated as if they are of the same type. A single interface or method call can be applied to objects from multiple class types seamlessly.
For example, think of the len()
function in Python. It can operate on a string, list, tuple, dictionary or other objects that implement the special __len__()
method:
print(len("Hello")) # 5
print(len([1, 2, 3])) # 3
print(len((1, 2))) # 2
The len()
function exhibits polymorphism as it works on different data type objects due to the presence of the common __len__()
method.
Some key advantages of polymorphism are:
- Code reuse - Common interfaces allow generic functions/methods to be written once and applied to different objects.
- Simpler code - Polymorphic code is easier to read, write and maintain.
- Extensibility - New object types can make use of existing polymorphic functions.
- Abstraction - Concrete implementation details are abstracted away from the code using the objects.
Enabling Polymorphism in Python
There are two main ways to enable polymorphic behavior in Python:
-
Abstract base classes - These define a generic interface, including abstract methods, that concrete subclasses inherit and implement.
-
Duck typing - Objects of different unrelated types are used polymorphically when they have compatible interfaces. Duck typing is a more informal type of polymorphism.
We will focus on abstract base classes, which provide a more strict and formal polymorphism mechanism.
Abstract Base Classes
Abstract base classes (ABCs) define a template with unimplemented abstract methods and properties that subclasses must override. This creates a common interface enforced on all subclasses.
Python provides the abc
module to help create abstract base classes.
For example, we can define a base class Animal
with an abstract make_sound()
method:
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def make_sound(self):
pass
This Animal
ABC defines the interface for all subclasses. Any concrete Animal
subclasses we now create must implement make_sound()
, otherwise they will raise a TypeError
.
Let’s define some subclasses:
class Dog(Animal):
def make_sound(self):
print("Bark!")
class Cat(Animal):
def make_sound(self):
print("Meow")
Now we can write polymorphic functions that use the common Animal
interface:
def animal_sound(animals: list[Animal]):
for animal in animals:
animal.make_sound()
dog = Dog()
cat = Cat()
animal_sound([dog, cat])
This will output:
Bark!
Meow
Even though Dog
and Cat
are different classes, we can iterate through them polymorphically using the common make_sound()
method from the Animal
interface.
ABC Syntax
The key aspects of writing abstract base classes in Python are:
-
Inherit from
ABC
to define a class as abstract:from abc import ABC class MyABC(ABC):
-
Use the
@abstractmethod
decorator to define abstract methods:@abstractmethod def abstract_method(self): pass
-
Abstract properties can be declared by setting them to
abc.abstractproperty
:my_prop = abc.abstractproperty`
-
Subclasses must override all abstract attributes with concrete implementations.
-
ABCs can also contain regular non-abstract concrete attributes and methods.
This syntax allows ABCs to clearly define the required interface for subclasses.
ABC Example - Graphical Shape Classes
Let’s see a more complete example of using ABCs to implement polymorphic classes.
We’ll define an abstract Shape
class with area()
and perimeter()
methods:
from abc import ABC, abstractmethod, abstractproperty
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
Then we can create concrete shape classes like Square
and Circle
:
from math import pi
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
def perimeter(self):
return 4 * self.side
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return pi * self.radius ** 2
def perimeter(self):
return 2 * pi * self.radius
And use them polymorphically:
square = Square(5)
circle = Circle(3)
shapes = [square, circle]
for shape in shapes:
print(shape.area())
print(shape.perimeter())
This will output the different areas and perimeters for the square and circle, allowing polymorphic use through their shared base Shape
interface.
The Shape
ABC enforces that all subclasses like Square
and Circle
must implement the area()
and perimeter()
methods. This guarantees the common interface required for polymorphism.
The collections.abc
Module
Python comes with several built-in abstract base classes for common data structures in the collections.abc
module:
Sequence
- For list, tuple, range sequencesMapping
- For dict mapsSet
- For set and frozenset setsIterable
- For iterable objects
For example, we can write a function to sum any Sequence
:
from collections.abc import Sequence
def sum_sequence(seq: Sequence) -> int:
total = 0
for x in seq:
total += x
return total
And apply it to different sequence types:
sum_sequence([1, 2, 3]) # 6
sum_sequence((4, 5, 6)) # 15
sum_sequence(range(7, 10)) # 24
The built-in collections ABCs like Sequence
provide convenient common interfaces for core Python types.
Interfaces in Python
While ABCs provide one way to create polymorphic class hierarchies, interfaces provide an alternative method without needing abstract classes.
Interfaces define the method signatures that classes must implement without any implementation. This separates the interface from the class hierarchy.
For example, we can define a simple Sounder
interface:
from abc import abstractmethod
class Sounder:
@abstractmethod
def make_sound(self):
pass
Then unrelated classes just need to implement this interface to enable polymorphic usage:
class Dog:
def make_sound(self):
print("Bark!")
class Car:
def make_sound(self):
print("Honk!")
def make_noise(sounder):
sounder.make_sound()
Here Dog
and Car
don’t inherit from a common base but implement the same Sounder
interface to be used polymorphically.
Interfaces are useful when you want to separate unrelated classes that just need to share common method signatures.
Method Overriding
A key aspect of polymorphism in OOP is method overriding. This means subclasses can override the implementation of methods inherited from parent classes.
For example:
class Animal:
def make_sound(self):
print("Some noise")
class Dog(Animal):
def make_sound(self):
print("Bark!")
Here Dog
overrides the general make_sound()
method to provide specialized behavior.
The version in Dog
will be called polymorphically even though make_sound()
originally comes from Animal
:
dog = Dog()
dog.make_sound() # "Bark!"
Method overriding allows subclasses to customize parent class methods to fit their needs. This helps enable polymorphic code using those methods.
Duck Typing
Duck typing is a concept related to polymorphism in dynamic languages like Python. It can be summed up by:
If it walks like a duck and talks like a duck, it’s a duck!
This means Python objects can be used polymorphically when they have the same interface or behaviors, even if they are of unrelated types. Like a duck!
For example, both str
and bytes
have an encode()
method. So they can be used interchangeably in that context:
def encode(obj):
print(obj.encode())
s = "Hello"
b = b"World"
encode(s)
encode(b) # No TypeError!
Duck typing does not use formal abstract classes or interfaces. It instead relies on implicit “duck-like” interfaces.
This provides loose polymorphic behavior between otherwise unrelated classes. But code may break if a class drops or changes its interface.
When to Use Polymorphism
Here are some common use cases where polymorphism is very handy in Python:
-
When you need the same method or functions to work on different kinds of objects or data types. For example, the built-in
len()
andstr()
functions. -
To create extensible frameworks and libraries that can handle custom subclasses without modification. For example, being able to iterate through a custom list subclass polymorphically.
-
To centralize interface definitions through ABCs while separating concrete implementations. This reduces coupling.
-
To write generic algorithmic code that can handle different kinds of objects, like sorting a list of polymorphic objects.
-
To swap components based on configuration or dependencies, such as switching database backends.
-
To write adapters that standardize disparate interfaces like APIs.
Overall, polymorphism helps create more reusable, maintainable and adaptable systems. But it should be used judiciously when there are clear benefits.
Polymorphism vs Overloading vs Overriding
It helps to differentiate polymorphism from two related concepts - overloading and overriding.
Overloading means having methods of the same name but different signatures in the same class. For example:
class Math:
def add(self, x, y):
# Handle x + y
def add(self, x, y, z):
# Handle x + y + z
Overriding means subclasses reimplementing methods inherited from parent classes. For example:
class Animal:
def make_sound(self):
print("Some noise")
class Dog(Animal):
def make_sound(self):
print("Bark!")
Polymorphism is about allowing different object types to be processed through a common interface. It relies on overriding to customize implementations across subclasses.
Practical Example - Game Characters
Let’s see a practical example of using ABCs and polymorphism to implement game character classes.
First, we define a Character
base class with shared attributes like name
and level
:
from abc import ABC, abstractmethod
class Character(ABC):
def __init__(self, name, level):
self.name = name
self.level = level
Next we create a PlayableCharacter
ABC that adds attack()
and heal()
methods:
class PlayableCharacter(Character, ABC):
@property
@abstractmethod
def stats(self): pass
@abstractmethod
def attack(self): pass
@abstractmethod
def heal(self): pass
Now we can extend this with specific character types:
from random import randint
class Warrior(PlayableCharacter):
def __init__(self, name, level):
super().__init__(name, level)
self.strength = randint(1, 10)
@property
def stats(self):
return {"strength": self.strength}
def attack(self):
return self.strength * 2
def heal(self):
pass
class Mage(PlayableCharacter):
def __init__(self, name, level):
super().__init__(name, level)
self.magic = randint(1, 10)
@property
def stats(self):
return {"magic": self.magic}
def attack(self):
return self.magic
def heal(self):
return self.magic * 1.5
Let’s create some characters:
warrior = Warrior("Throgdar", 5)
mage = Mage("Elminster", 8)
And now we can process them generically through the base Character
interface:
characters = [warrior, mage]
for character in characters:
print(character.name)
print(character.level)
But also use the subclasses polymorphically:
for character in characters:
print(character.name, "attack:", character.attack())
print(character.name, "heal:", character.heal())
This allows both shared processing through the base as well as subclass-specific polymorphic behavior.
Conclusion
Polymorphism is a powerful concept in object-oriented programming and Python makes it easy to implement polymorphic behavior using abstract base classes or duck typing.
Key points:
-
Polymorphism allows a single interface to be used with different object types seamlessly.
-
ABCs define abstract interfaces and enforce them on subclasses.
-
Interfaces provide an alternative to ABCs for sharing method signatures.
-
Duck typing enables informal polymorphic usage when objects have compatible interfaces.
-
Method overriding allows subclasses to customize inherited behavior.
-
Use polymorphism when you need code flexibility, extensibility and reusable abstractions.
I hope this comprehensive guide gives you a deep understanding of implementing polymorphism in Python using abstract classes and other techniques! Let me know if you have any other questions.