Polymorphism: Flexible Interfaces
Welcome to Polymorphism! Think of it as shape-shifting - the same method name can behave differently depending on the object type. It’s like having a universal remote that works with any TV brand.
What is Polymorphism?
Polymorphism means “many forms.” In OOP, it allows objects of different classes to be treated as objects of a common parent class. The same method call can produce different results based on the actual object type.
Method Overriding (Runtime Polymorphism)
class Animal:
def make_sound(self):
return "Some generic sound"
class Dog(Animal):
def make_sound(self): # Override parent method
return "Woof!"
class Cat(Animal):
def make_sound(self):
return "Meow!"
class Bird(Animal):
def make_sound(self):
return "Tweet!"
# Polymorphism in action
animals = [Dog(), Cat(), Bird(), Animal()]
for animal in animals:
print(f"{animal.__class__.__name__}: {animal.make_sound()}")
# Output:
# Dog: Woof!
# Cat: Meow!
# Bird: Tweet!
# Animal: Some generic sound
Duck Typing
Python’s Dynamic Polymorphism
In Python, polymorphism often works through duck typing - “If it walks like a duck and quacks like a duck, then it must be a duck.”
class Duck:
def quack(self):
return "Quack!"
def walk(self):
return "Waddles"
class Person:
def quack(self):
return "Person imitating duck: Quack!"
def walk(self):
return "Walks on two legs"
def make_it_quack(thing):
"""Works with any object that has a quack() method."""
return thing.quack()
def make_it_move(thing):
"""Works with any object that has a walk() method."""
return thing.walk()
# Both work the same way
duck = Duck()
person = Person()
print(make_it_quack(duck)) # "Quack!"
print(make_it_quack(person)) # "Person imitating duck: Quack!"
print(make_it_move(duck)) # "Waddles"
print(make_it_move(person)) # "Walks on two legs"
Operator Overloading
Special Methods for Operators
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other): # Overload +
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
return NotImplemented
def __sub__(self, other): # Overload -
if isinstance(other, Vector):
return Vector(self.x - other.x, self.y - other.y)
return NotImplemented
def __mul__(self, scalar): # Overload *
if isinstance(scalar, (int, float)):
return Vector(self.x * scalar, self.y * scalar)
return NotImplemented
def __eq__(self, other): # Overload ==
if isinstance(other, Vector):
return self.x == other.x and self.y == other.y
return False
def __str__(self): # Overload str()
return f"Vector({self.x}, {self.y})"
def __repr__(self): # Overload repr()
return f"Vector({self.x}, {self.y})"
# Test operator overloading
v1 = Vector(2, 3)
v2 = Vector(1, 4)
print(f"v1: {v1}")
print(f"v2: {v2}")
print(f"v1 + v2: {v1 + v2}")
print(f"v1 - v2: {v1 - v2}")
print(f"v1 * 3: {v1 * 3}")
print(f"v1 == v2: {v1 == v2}")
print(f"v1 == Vector(2, 3): {v1 == Vector(2, 3)}")
Abstract Base Classes and Polymorphism
Using abc for Interface Contracts
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
@abstractmethod
def process_payment(self, amount):
pass
@abstractmethod
def get_payment_info(self):
pass
class CreditCard(PaymentMethod):
def __init__(self, card_number, expiry_date):
self.card_number = card_number
self.expiry_date = expiry_date
def process_payment(self, amount):
return f"Processed ${amount} via credit card ****{self.card_number[-4:]}"
def get_payment_info(self):
return f"Credit Card ending in {self.card_number[-4:]}"
class PayPal(PaymentMethod):
def __init__(self, email):
self.email = email
def process_payment(self, amount):
return f"Processed ${amount} via PayPal account {self.email}"
def get_payment_info(self):
return f"PayPal account {self.email}"
class BankTransfer(PaymentMethod):
def __init__(self, account_number, routing_number):
self.account_number = account_number
self.routing_number = routing_number
def process_payment(self, amount):
return f"Processed ${amount} via bank transfer to account {self.account_number}"
def get_payment_info(self):
return f"Bank account {self.account_number}"
# Polymorphic payment processing
def checkout(payment_method, amount):
"""Works with any PaymentMethod subclass."""
print(f"Payment info: {payment_method.get_payment_info()}")
result = payment_method.process_payment(amount)
print(f"Result: {result}")
return result
# Test polymorphism
payments = [
CreditCard("4111111111111111", "12/25"),
PayPal("user@example.com"),
BankTransfer("123456789", "021000021")
]
for payment in payments:
checkout(payment, 99.99)
print()
Container Protocol
Making Objects Work Like Containers
class ShoppingCart:
def __init__(self):
self._items = []
def add_item(self, item, quantity=1):
self._items.append((item, quantity))
def __len__(self): # len(cart)
return len(self._items)
def __getitem__(self, index): # cart[index]
return self._items[index]
def __setitem__(self, index, value): # cart[index] = value
self._items[index] = value
def __delitem__(self, index): # del cart[index]
del self._items[index]
def __iter__(self): # for item in cart:
return iter(self._items)
def __contains__(self, item): # item in cart
return any(cart_item[0] == item for cart_item in self._items)
def __str__(self):
return f"ShoppingCart with {len(self)} items"
# Test container protocol
cart = ShoppingCart()
cart.add_item("Apple", 3)
cart.add_item("Banana", 2)
cart.add_item("Orange", 1)
print(f"Cart: {cart}")
print(f"Length: {len(cart)}")
print(f"First item: {cart[0]}")
print(f"Contains 'Apple': {'Apple' in cart}")
print(f"Contains 'Grape': {'Grape' in cart}")
print("All items:")
for item, quantity in cart:
print(f" {item}: {quantity}")
Comparison Operators
Implementing Rich Comparisons
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade
def __eq__(self, other): # ==
if isinstance(other, Student):
return self.grade == other.grade
return NotImplemented
def __lt__(self, other): # <
if isinstance(other, Student):
return self.grade < other.grade
return NotImplemented
def __le__(self, other): # <=
return self < other or self == other
def __gt__(self, other): # >
return not (self <= other)
def __ge__(self, other): # >=
return not (self < other)
def __str__(self):
return f"{self.name} (Grade: {self.grade})"
# Test comparisons
students = [
Student("Alice", 85),
Student("Bob", 92),
Student("Charlie", 78),
Student("Diana", 85)
]
print("Students sorted by grade:")
for student in sorted(students):
print(f" {student}")
alice = students[0]
diana = students[3]
print(f"\n{alice.name} == {diana.name}: {alice == diana}")
print(f"{alice.name} < {diana.name}: {alice < diana}")
Context Managers
__enter__ and __exit__ Methods
class DatabaseConnection:
def __init__(self, db_name):
self.db_name = db_name
self.connected = False
def __enter__(self):
print(f"Connecting to {self.db_name}...")
self.connected = True
return self # This becomes the 'as' variable
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"Disconnecting from {self.db_name}...")
self.connected = False
# Return False to propagate exceptions, True to suppress
def query(self, sql):
if not self.connected:
raise RuntimeError("Not connected to database")
return f"Executed: {sql}"
# Test context manager
with DatabaseConnection("mydb") as db:
result = db.query("SELECT * FROM users")
print(result)
print(f"Connected: {db.connected}") # False - automatically disconnected
Function Polymorphism
Functions That Work with Multiple Types
def calculate_area(shape):
"""Works with any shape that has an area() method."""
return shape.area()
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
class Triangle:
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return 0.5 * self.base * self.height
# Test polymorphic function
shapes = [
Rectangle(5, 3),
Circle(4),
Triangle(6, 4)
]
for shape in shapes:
print(f"{shape.__class__.__name__} area: {calculate_area(shape)}")
Practical Examples
Example 1: Game Character System
from abc import ABC, abstractmethod
class Character(ABC):
def __init__(self, name, health):
self.name = name
self.health = health
@abstractmethod
def attack(self):
pass
@abstractmethod
def defend(self):
pass
def take_damage(self, damage):
self.health -= damage
if self.health <= 0:
print(f"{self.name} has been defeated!")
else:
print(f"{self.name} takes {damage} damage, {self.health} health remaining")
class Warrior(Character):
def attack(self):
return f"{self.name} swings sword for 15 damage!"
def defend(self):
return f"{self.name} raises shield, reducing damage by 5"
class Mage(Character):
def attack(self):
return f"{self.name} casts fireball for 20 damage!"
def defend(self):
return f"{self.name} creates magic barrier, reducing damage by 8"
class Archer(Character):
def attack(self):
return f"{self.name} shoots arrow for 12 damage!"
def defend(self):
return f"{self.name} dodges, reducing damage by 3"
def battle(attacker, defender):
"""Polymorphic battle function - works with any Character subclass."""
print(attacker.attack())
print(defender.defend())
# Simulate damage calculation
base_damage = {"Warrior": 15, "Mage": 20, "Archer": 12}[attacker.__class__.__name__]
defense = {"Warrior": 5, "Mage": 8, "Archer": 3}[defender.__class__.__name__]
actual_damage = max(0, base_damage - defense)
defender.take_damage(actual_damage)
print()
# Test polymorphism
warrior = Warrior("Conan", 100)
mage = Mage("Merlin", 80)
archer = Archer("Robin", 90)
battle(warrior, mage)
battle(mage, archer)
battle(archer, warrior)
Example 2: File Processor System
from abc import ABC, abstractmethod
import os
class FileProcessor(ABC):
def __init__(self, file_path):
self.file_path = file_path
@abstractmethod
def process(self):
pass
@abstractmethod
def get_info(self):
pass
def exists(self):
return os.path.exists(self.file_path)
class TextFileProcessor(FileProcessor):
def process(self):
with open(self.file_path, 'r') as f:
content = f.read()
return content.upper()
def get_info(self):
lines = 0
words = 0
chars = 0
with open(self.file_path, 'r') as f:
for line in f:
lines += 1
words += len(line.split())
chars += len(line)
return f"Text file: {lines} lines, {words} words, {chars} characters"
class ImageFileProcessor(FileProcessor):
def process(self):
# Simulate image processing
return f"Processed image: {self.file_path} (resized and filtered)"
def get_info(self):
# Simulate getting image info
return f"Image file: {self.file_path} (1920x1080, JPEG)"
class CSVFileProcessor(FileProcessor):
def process(self):
import csv
data = []
with open(self.file_path, 'r') as f:
reader = csv.reader(f)
for row in reader:
data.append(row)
return f"Processed CSV with {len(data)} rows"
def get_info(self):
rows = 0
columns = 0
with open(self.file_path, 'r') as f:
reader = csv.reader(f)
for i, row in enumerate(reader):
rows = i + 1
columns = len(row)
return f"CSV file: {rows} rows, {columns} columns"
def process_files(processors):
"""Polymorphic function that works with any FileProcessor."""
results = []
for processor in processors:
if processor.exists():
info = processor.get_info()
result = processor.process()
results.append(f"{info}\n{result}")
else:
results.append(f"File not found: {processor.file_path}")
return results
# Test polymorphism
processors = [
TextFileProcessor("document.txt"),
ImageFileProcessor("photo.jpg"),
CSVFileProcessor("data.csv")
]
# Create sample files for testing
with open("document.txt", "w") as f:
f.write("Hello World\nThis is a test file.")
with open("data.csv", "w") as f:
f.write("Name,Age,City\nAlice,25,NYC\nBob,30,LA")
results = process_files(processors)
for result in results:
print(result)
print("-" * 40)
Example 3: Notification System
from abc import ABC, abstractmethod
class NotificationService(ABC):
@abstractmethod
def send(self, message, recipient):
pass
class EmailService(NotificationService):
def send(self, message, recipient):
return f"Email sent to {recipient}: {message}"
class SMSService(NotificationService):
def send(self, message, recipient):
# Truncate message for SMS
truncated = message[:160] + "..." if len(message) > 160 else message
return f"SMS sent to {recipient}: {truncated}"
class PushNotificationService(NotificationService):
def send(self, message, recipient):
return f"Push notification sent to {recipient}: {message}"
class NotificationManager:
def __init__(self):
self.services = {}
def register_service(self, service_type, service):
self.services[service_type] = service
def notify(self, service_type, message, recipient):
"""Polymorphic notification method."""
if service_type in self.services:
service = self.services[service_type]
return service.send(message, recipient)
else:
return f"Unknown service type: {service_type}"
# Test polymorphism
manager = NotificationManager()
manager.register_service("email", EmailService())
manager.register_service("sms", SMSService())
manager.register_service("push", PushNotificationService())
message = "Your order has been shipped and will arrive in 2-3 business days. " * 5 # Long message
notifications = [
("email", "user@example.com"),
("sms", "555-0123"),
("push", "device_token_123")
]
for service_type, recipient in notifications:
result = manager.notify(service_type, message, recipient)
print(result)
print()
Practice Exercises
Exercise 1: Shape Drawing System
Create a polymorphic drawing system:
- Base
Shapeclass withdraw()method Circle,Rectangle,Trianglesubclasses- Different drawing implementations
- Unified drawing function
Exercise 2: Animal Sound Simulator
Build an animal sound system:
- Base
Animalclass - Different animal subclasses with unique sounds
make_sound()method polymorphism- Zoo simulation with mixed animals
Exercise 3: Payment Processing System
Create a payment processing system:
- Abstract
PaymentMethodclass CreditCard,PayPal,Bitcoinimplementations- Polymorphic
process_payment()method - Shopping cart integration
Exercise 4: File Compression System
Build a file compression system:
- Base
Compressorclass ZipCompressor,GzipCompressor,TarCompressor- Polymorphic compression/decompression
- File size reporting
Exercise 5: Game Power-up System
Create a game power-up system:
- Base
PowerUpclass SpeedBoost,HealthPack,WeaponUpgradesubclasses- Polymorphic
activate()method - Player interaction system
Summary
Polymorphism enables flexible, interchangeable object interfaces:
Method Overriding:
class Parent:
def method(self):
return "Parent implementation"
class Child(Parent):
def method(self): # Override
return "Child implementation"
Duck Typing:
def func(obj):
return obj.quack() # Works with any object that has quack()
Operator Overloading:
class MyClass:
def __add__(self, other): # Overload +
return MyClass(self.value + other.value)
Abstract Base Classes:
from abc import ABC, abstractmethod
class AbstractClass(ABC):
@abstractmethod
def required_method(self):
pass
Key Benefits:
- Code reusability
- Extensibility
- Interface consistency
- Runtime flexibility
Common Polymorphic Patterns:
- Strategy pattern (different algorithms)
- Factory pattern (object creation)
- Observer pattern (event handling)
- Command pattern (action encapsulation)
Next: Special Methods - making your classes magical! ✨