Polymorphism
Polymorphism is a core concept in object-oriented programming that allows objects of different classes to be treated as objects of a common superclass. The term comes from Greek words meaning “many forms,” and that’s exactly what polymorphism allows: a single interface to entities of different types.
Understanding Polymorphism
In Python, polymorphism allows you to define methods in different classes with the same name, enabling you to call the same method on different objects and get different results. This promotes code reusability and flexibility.
There are several ways polymorphism manifests in Python:
1. Method Overriding
Method overriding occurs when a subclass provides a specific implementation of a method that is already defined in its parent class. When you call the method on an instance of the subclass, the overridden method in the subclass is executed instead of the parent class method.
class Animal:
def speak(self):
print("Animal makes a sound")
class Dog(Animal):
def speak(self):
print("Dog barks")
class Cat(Animal):
def speak(self):
print("Cat meows")
# Create instances
animal = Animal()
dog = Dog()
cat = Cat()
# Call the speak method on each object
animal.speak() # Output: Animal makes a sound
dog.speak() # Output: Dog barks
cat.speak() # Output: Cat meows
In this example, the speak()
method is defined in the Animal
class and overridden in both the Dog
and Cat
subclasses. Each class provides its own implementation of the method, demonstrating polymorphism through method overriding.
2. Duck Typing
Python’s approach to polymorphism is closely tied to the concept of “duck typing,” which follows the principle: “If it walks like a duck and quacks like a duck, then it probably is a duck.” This means Python doesn’t care about the type of an object, only that it implements the methods or attributes being accessed.
class Dog:
def speak(self):
print("Woof!")
class Cat:
def speak(self):
print("Meow!")
class Duck:
def speak(self):
print("Quack!")
def make_speak(animal):
animal.speak()
# Create instances
dog = Dog()
cat = Cat()
duck = Duck()
# Use polymorphism through duck typing
make_speak(dog) # Output: Woof!
make_speak(cat) # Output: Meow!
make_speak(duck) # Output: Quack!
In this example, the make_speak()
function doesn’t care about the type of animal it receives; it only cares that the object has a speak()
method. This is duck typing in action: as long as the object implements the expected interface, it will work.
Note: Unlike statically-typed languages like Java or C++, Python doesn’t require objects to explicitly inherit from a common superclass to use polymorphism. Duck typing provides a more flexible approach.
3. Method Overloading (Simulated)
Method overloading allows a class to have multiple methods with the same name but different parameters. Python doesn’t support traditional method overloading where you define multiple methods with the same name in the same class. However, you can simulate it using default arguments or variable parameters.
class Calculator:
def add(self, *args):
result = 0
for arg in args:
result += arg
return result
# Create a calculator instance
calc = Calculator()
# Call add with different numbers of arguments
print(calc.add(2, 3)) # Output: 5
print(calc.add(2, 3, 4)) # Output: 9
print(calc.add(1, 2, 3, 4)) # Output: 10
This example simulates method overloading by using variable arguments (*args
). The add()
method can accept any number of arguments, demonstrating polymorphic behavior.
4. Abstract Base Classes (ABC)
Python provides the abc
module for working with abstract base classes, which are classes that cannot be instantiated and are designed to be subclassed. Abstract methods defined in an abstract base class must be implemented by its subclasses, providing a formal way to enforce polymorphism.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
import math
return math.pi * (self.radius ** 2)
def perimeter(self):
import math
return 2 * math.pi * self.radius
# Create instances
rectangle = Rectangle(5, 4)
circle = Circle(3)
# Calculate and display areas
print(f"Rectangle area: {rectangle.area()}") # Output: Rectangle area: 20
print(f"Circle area: {circle.area():.2f}") # Output: Circle area: 28.27
print(f"Rectangle perimeter: {rectangle.perimeter()}") # Output: Rectangle perimeter: 18
print(f"Circle perimeter: {circle.perimeter():.2f}") # Output: Circle perimeter: 18.85
In this example, the abstract base class Shape
defines the interface with abstract methods area()
and perimeter()
. The concrete subclasses Rectangle
and Circle
must implement these methods, ensuring a consistent interface but allowing for different implementations.
Important:
Attempting to instantiate the abstract base class directly would raise a TypeError
, as abstract classes cannot be instantiated.
Practical Example: Building a Multimedia Player
Let’s look at a more comprehensive example of polymorphism in a multimedia player application:
from abc import ABC, abstractmethod
class MediaFile(ABC):
def __init__(self, filename):
self.filename = filename
@abstractmethod
def play(self):
pass
@abstractmethod
def display_info(self):
pass
def get_filename(self):
return self.filename
class AudioFile(MediaFile):
def __init__(self, filename, artist, title, duration):
super().__init__(filename)
self.artist = artist
self.title = title
self.duration = duration
def play(self):
print(f"Playing audio: {self.title} by {self.artist}")
# Code to actually play the audio would go here
def display_info(self):
print(f"Audio: {self.title}")
print(f"Artist: {self.artist}")
print(f"Duration: {self.format_duration()}")
print(f"File: {self.filename}")
def format_duration(self):
minutes = self.duration // 60
seconds = self.duration % 60
return f"{minutes}:{seconds:02d}"
class VideoFile(MediaFile):
def __init__(self, filename, title, duration, resolution):
super().__init__(filename)
self.title = title
self.duration = duration
self.resolution = resolution
def play(self):
print(f"Playing video: {self.title} ({self.resolution})")
# Code to actually play the video would go here
def display_info(self):
print(f"Video: {self.title}")
print(f"Resolution: {self.resolution}")
print(f"Duration: {self.format_duration()}")
print(f"File: {self.filename}")
def format_duration(self):
minutes = self.duration // 60
seconds = self.duration % 60
return f"{minutes}:{seconds:02d}"
class ImageFile(MediaFile):
def __init__(self, filename, title, resolution):
super().__init__(filename)
self.title = title
self.resolution = resolution
def play(self):
print(f"Displaying image: {self.title}")
# Code to display the image would go here
def display_info(self):
print(f"Image: {self.title}")
print(f"Resolution: {self.resolution}")
print(f"File: {self.filename}")
class MediaPlayer:
def __init__(self):
self.media_library = []
def add_media(self, media):
if isinstance(media, MediaFile):
self.media_library.append(media)
print(f"Added {media.get_filename()} to library")
else:
print("Error: Not a valid media file")
def play_all(self):
print("\nPlaying all media:")
for media in self.media_library:
media.play()
def display_library(self):
print("\nMedia Library:")
for i, media in enumerate(self.media_library, 1):
print(f"\nItem {i}:")
media.display_info()
print("-" * 20)
# Create media files
song = AudioFile("song.mp3", "The Beatles", "Hey Jude", 243)
movie = VideoFile("movie.mp4", "The Matrix", 8280, "1080p")
photo = ImageFile("photo.jpg", "Vacation", "4K")
# Create media player and add files
player = MediaPlayer()
player.add_media(song)
player.add_media(movie)
player.add_media(photo)
# Use the media player
player.display_library()
player.play_all()
Output:
Added song.mp3 to library
Added movie.mp4 to library
Added photo.jpg to library
Media Library:
Item 1:
Audio: Hey Jude
Artist: The Beatles
Duration: 4:03
File: song.mp3
--------------------
Item 2:
Video: The Matrix
Resolution: 1080p
Duration: 138:00
File: movie.mp4
--------------------
Item 3:
Image: Vacation
Resolution: 4K
File: photo.jpg
--------------------
Playing all media:
Playing audio: Hey Jude by The Beatles
Playing video: The Matrix (1080p)
Displaying image: Vacation
In this example:
- The abstract base class
MediaFile
defines a common interface with methods likeplay()
anddisplay_info()
. - Different types of media (audio, video, image) implement these methods in their own way.
- The
MediaPlayer
class can work with any type of media as long as it adheres to theMediaFile
interface. - When we call
play_all()
, we see polymorphism in action as each media type responds differently to the same method call.
Benefits of Polymorphism
Code Reusability: Polymorphism allows you to write methods that can operate on objects of various types, reducing code duplication.
Extensibility: New classes can be added to the system without modifying existing code, as long as they implement the required interface.
Flexibility: Systems can be designed to work with objects at a more abstract level, focusing on what operations are performed rather than how they are performed.
Maintenance: Changes to one class don’t affect other classes, making the code easier to maintain.
Common Pitfalls and Best Practices
Pitfall 1: Ignoring the Liskov Substitution Principle
The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.
# Problematic
class Bird:
def fly(self):
print("I can fly")
class Penguin(Bird):
def fly(self):
raise Exception("Penguins can't fly!") # Violates LSP
# Better approach
class Bird:
def move(self):
print("I can move")
class FlyingBird(Bird):
def move(self):
print("I can fly")
class SwimmingBird(Bird):
def move(self):
print("I can swim")
Best Practice 1: Use Abstract Base Classes for Complex Hierarchies
For complex class hierarchies, use the abc
module to create abstract base classes that define the required interface.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def draw(self):
pass
Best Practice 2: Follow the “Tell, Don’t Ask” Principle
Instead of checking the type of an object and then calling different methods, let polymorphism handle the variations.
# Avoid this
def process_shape(shape):
if isinstance(shape, Circle):
shape.draw_circle()
elif isinstance(shape, Rectangle):
shape.draw_rectangle()
# Prefer this
def process_shape(shape):
shape.draw() # Polymorphic method call
Best Practice 3: Use Duck Typing for Flexibility
Embrace Python’s duck typing to create flexible code that works with any object that supports the required operations.
def process_data(data_source):
# As long as data_source has a read() method, this will work
content = data_source.read()
return process_content(content)
# Can be used with files, network streams, StringIO objects, etc.
Exercises
Exercise 1: Create a base class Vehicle
with a method move()
. Then create subclasses for Car
, Boat
, and Airplane
, each overriding the move()
method to describe how that vehicle moves. Then create a function that takes a vehicle and makes it move.
Exercise 2: Implement a simple polymorphic system for different types of employees (regular employees, managers, and executives) with a common method calculate_salary()
that works differently for each employee type.
Exercise 3: Create an abstract base class PaymentMethod
with an abstract method process_payment(amount)
. Then implement concrete classes CreditCard
, PayPal
, and BankTransfer
that each implement the payment processing differently. Finally, create a ShoppingCart
class that can process a payment using any payment method.
Hint for Exercise 1:
class Vehicle:
def move(self):
pass
class Car(Vehicle):
def move(self):
return "The car drives on the road"
# Complete the other classes and create a function that demonstrates polymorphism
In the next sections of Object-Oriented Programming, we’ll dive deeper into encapsulation and explore advanced concepts like magic methods, properties, and class decorators.