Python Polymorphism
Learn about polymorphism in Python - the ability of different objects to respond to the same interface.
What is Polymorphism?
Polymorphism is a core concept in object-oriented programming that allows objects of different types to be treated as objects of a common base type.
The word "polymorphism" comes from Greek, meaning "many forms". In programming, it refers to the ability of different objects to respond to the same method call in their own specific way.
Python supports polymorphism through method overriding, duck typing, and operator overloading.
Method Polymorphism
Different classes can have methods with the same name, but different implementations:
Example - Basic polymorphism with animals:
class Dog:
def make_sound(self):
return "Woof!"
def move(self):
return "Running on four legs"
class Cat:
def make_sound(self):
return "Meow!"
def move(self):
return "Sneaking quietly"
class Bird:
def make_sound(self):
return "Tweet!"
def move(self):
return "Flying in the sky"
# Polymorphic function
def animal_sounds(animals):
for animal in animals:
print(f"Animal says: {animal.make_sound()}")
print(f"Animal moves: {animal.move()}")
print()
# Usage
dog = Dog()
cat = Cat()
bird = Bird()
animals = [dog, cat, bird]
animal_sounds(animals) Polymorphism with Inheritance
Polymorphism is often used with inheritance, where child classes override parent methods:
Example - Shape hierarchy with polymorphism:
import math
class Shape:
def __init__(self, name):
self.name = name
def area(self):
raise NotImplementedError("Subclass must implement area method")
def perimeter(self):
raise NotImplementedError("Subclass must implement perimeter method")
def describe(self):
return f"This is a {self.name}"
class Rectangle(Shape):
def __init__(self, width, height):
super().__init__("Rectangle")
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):
super().__init__("Circle")
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
class Triangle(Shape):
def __init__(self, side1, side2, side3):
super().__init__("Triangle")
self.side1 = side1
self.side2 = side2
self.side3 = side3
def area(self):
# Using Heron's formula
s = self.perimeter() / 2
return math.sqrt(s * (s - self.side1) * (s - self.side2) * (s - self.side3))
def perimeter(self):
return self.side1 + self.side2 + self.side3
# Polymorphic function
def calculate_shape_properties(shapes):
total_area = 0
total_perimeter = 0
for shape in shapes:
print(shape.describe())
area = shape.area()
perimeter = shape.perimeter()
print(f"Area: {area:.2f}")
print(f"Perimeter: {perimeter:.2f}")
print()
total_area += area
total_perimeter += perimeter
print(f"Total area: {total_area:.2f}")
print(f"Total perimeter: {total_perimeter:.2f}")
# Usage
rectangle = Rectangle(5, 3)
circle = Circle(4)
triangle = Triangle(3, 4, 5)
shapes = [rectangle, circle, triangle]
calculate_shape_properties(shapes) Duck Typing
Python uses "duck typing" - if it walks like a duck and quacks like a duck, it's a duck. Objects don't need to inherit from the same class to be used polymorphically:
Example - Duck typing with different classes:
class Duck:
def fly(self):
return "Duck flying"
def swim(self):
return "Duck swimming"
class Airplane:
def fly(self):
return "Airplane flying"
class Fish:
def swim(self):
return "Fish swimming"
class Boat:
def swim(self):
return "Boat floating on water"
# Polymorphic functions using duck typing
def make_it_fly(flying_object):
try:
return flying_object.fly()
except AttributeError:
return f"{type(flying_object).__name__} can't fly"
def make_it_swim(swimming_object):
try:
return swimming_object.swim()
except AttributeError:
return f"{type(swimming_object).__name__} can't swim"
# Usage
duck = Duck()
airplane = Airplane()
fish = Fish()
boat = Boat()
flying_things = [duck, airplane, fish]
swimming_things = [duck, fish, boat, airplane]
print("Flying:")
for thing in flying_things:
print(make_it_fly(thing))
print("\nSwimming:")
for thing in swimming_things:
print(make_it_swim(thing)) Operator Overloading
Python allows you to define how operators work with your custom objects through special methods:
Example - Vector class with operator overloading:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"Vector({self.x}, {self.y})"
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __add__(self, other):
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
return NotImplemented
def __sub__(self, other):
if isinstance(other, Vector):
return Vector(self.x - other.x, self.y - other.y)
return NotImplemented
def __mul__(self, scalar):
if isinstance(scalar, (int, float)):
return Vector(self.x * scalar, self.y * scalar)
return NotImplemented
def __rmul__(self, scalar):
return self.__mul__(scalar)
def __eq__(self, other):
if isinstance(other, Vector):
return self.x == other.x and self.y == other.y
return False
def __abs__(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
def __bool__(self):
return self.x != 0 or self.y != 0
# Usage
v1 = Vector(3, 4)
v2 = Vector(1, 2)
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"2 * v1: {2 * v1}")
print(f"v1 == v2: {v1 == v2}")
print(f"abs(v1): {abs(v1)}")
print(f"bool(v1): {bool(v1)}")
print(f"bool(Vector(0, 0)): {bool(Vector(0, 0))}") Abstract Base Classes
Use abstract base classes to define interfaces that ensure polymorphic behavior:
Example - Payment processing system:
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount):
pass
@abstractmethod
def validate_payment_info(self):
pass
def log_transaction(self, amount, status):
print(f"Transaction: ${amount:.2f} - Status: {status}")
class CreditCardProcessor(PaymentProcessor):
def __init__(self, card_number, cvv, expiry_date):
self.card_number = card_number
self.cvv = cvv
self.expiry_date = expiry_date
def validate_payment_info(self):
# Simplified validation
return len(self.card_number) == 16 and len(self.cvv) == 3
def process_payment(self, amount):
if self.validate_payment_info():
# Simulate payment processing
self.log_transaction(amount, "SUCCESS")
return True
else:
self.log_transaction(amount, "FAILED - Invalid card info")
return False
class PayPalProcessor(PaymentProcessor):
def __init__(self, email, password):
self.email = email
self.password = password
def validate_payment_info(self):
return "@" in self.email and len(self.password) >= 8
def process_payment(self, amount):
if self.validate_payment_info():
self.log_transaction(amount, "SUCCESS")
return True
else:
self.log_transaction(amount, "FAILED - Invalid PayPal credentials")
return False
class BankTransferProcessor(PaymentProcessor):
def __init__(self, account_number, routing_number):
self.account_number = account_number
self.routing_number = routing_number
def validate_payment_info(self):
return len(self.account_number) >= 8 and len(self.routing_number) == 9
def process_payment(self, amount):
if self.validate_payment_info():
self.log_transaction(amount, "SUCCESS")
return True
else:
self.log_transaction(amount, "FAILED - Invalid bank info")
return False
# Polymorphic payment processing
def process_payments(processors, amount):
successful_payments = 0
for processor in processors:
print(f"\nProcessing payment with {type(processor).__name__}")
if processor.process_payment(amount):
successful_payments += 1
print(f"\nSuccessful payments: {successful_payments}/{len(processors)}")
# Usage
credit_card = CreditCardProcessor("1234567890123456", "123", "12/25")
paypal = PayPalProcessor("user@example.com", "password123")
bank_transfer = BankTransferProcessor("12345678", "123456789")
processors = [credit_card, paypal, bank_transfer]
process_payments(processors, 100.00) Polymorphism with Built-in Functions
Many built-in Python functions work polymorphically with different types:
Example - len() function polymorphism:
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __len__(self):
return len(self.songs)
def __str__(self):
return f"Playlist '{self.name}' with {len(self)} songs"
class Book:
def __init__(self, title, pages):
self.title = title
self.pages = pages
def __len__(self):
return self.pages
def __str__(self):
return f"Book '{self.title}' with {len(self)} pages"
# Polymorphic use of len()
items = [
[1, 2, 3, 4, 5], # List
"Hello World", # String
{"a": 1, "b": 2}, # Dictionary
Playlist("My Favorites"), # Custom class
Book("Python Guide", 300) # Custom class
]
# Add some songs to playlist
items[3].add_song("Song 1")
items[3].add_song("Song 2")
items[3].add_song("Song 3")
for item in items:
print(f"Length of {type(item).__name__}: {len(item)}")
if hasattr(item, '__str__'):
print(f" Details: {item}")
print() Real-World Example: Media Player
Complete polymorphism example:
from abc import ABC, abstractmethod
import time
class MediaFile(ABC):
def __init__(self, filename, duration):
self.filename = filename
self.duration = duration
self.position = 0
self.is_playing = False
@abstractmethod
def play(self):
pass
@abstractmethod
def get_info(self):
pass
def pause(self):
self.is_playing = False
return f"Paused {self.filename}"
def stop(self):
self.is_playing = False
self.position = 0
return f"Stopped {self.filename}"
def seek(self, position):
if 0 <= position <= self.duration:
self.position = position
return f"Seeked to {position}s in {self.filename}"
return "Invalid position"
class AudioFile(MediaFile):
def __init__(self, filename, duration, artist, album):
super().__init__(filename, duration)
self.artist = artist
self.album = album
def play(self):
self.is_playing = True
return f"Playing audio: {self.filename} by {self.artist}"
def get_info(self):
return {
"type": "Audio",
"filename": self.filename,
"artist": self.artist,
"album": self.album,
"duration": self.duration
}
class VideoFile(MediaFile):
def __init__(self, filename, duration, resolution, fps):
super().__init__(filename, duration)
self.resolution = resolution
self.fps = fps
def play(self):
self.is_playing = True
return f"Playing video: {self.filename} ({self.resolution})"
def get_info(self):
return {
"type": "Video",
"filename": self.filename,
"resolution": self.resolution,
"fps": self.fps,
"duration": self.duration
}
class PodcastFile(MediaFile):
def __init__(self, filename, duration, host, episode_number):
super().__init__(filename, duration)
self.host = host
self.episode_number = episode_number
def play(self):
self.is_playing = True
return f"Playing podcast: Episode {self.episode_number} hosted by {self.host}"
def get_info(self):
return {
"type": "Podcast",
"filename": self.filename,
"host": self.host,
"episode": self.episode_number,
"duration": self.duration
}
class MediaPlayer:
def __init__(self):
self.playlist = []
self.current_index = 0
def add_media(self, media_file):
self.playlist.append(media_file)
def play_current(self):
if self.playlist and 0 <= self.current_index < len(self.playlist):
current_media = self.playlist[self.current_index]
return current_media.play()
return "No media to play"
def next_track(self):
if self.current_index < len(self.playlist) - 1:
self.current_index += 1
return self.play_current()
return "End of playlist"
def previous_track(self):
if self.current_index > 0:
self.current_index -= 1
return self.play_current()
return "Beginning of playlist"
def show_playlist(self):
print("Playlist:")
for i, media in enumerate(self.playlist):
marker = "▶ " if i == self.current_index else " "
info = media.get_info()
print(f"{marker}{i+1}. {info['filename']} ({info['type']})")
def get_current_info(self):
if self.playlist and 0 <= self.current_index < len(self.playlist):
return self.playlist[self.current_index].get_info()
return None
# Usage
player = MediaPlayer()
# Add different types of media files
audio = AudioFile("song.mp3", 180, "The Beatles", "Abbey Road")
video = VideoFile("movie.mp4", 7200, "1920x1080", 24)
podcast = PodcastFile("tech_talk.mp3", 3600, "John Doe", 42)
player.add_media(audio)
player.add_media(video)
player.add_media(podcast)
# Polymorphic operations
print(player.play_current())
print()
player.show_playlist()
print()
print("Current media info:")
current_info = player.get_current_info()
for key, value in current_info.items():
print(f" {key}: {value}")
print()
print(player.next_track())
print(player.next_track())
print(player.previous_track()) Benefits of Polymorphism
- Code Reusability: Write functions that work with multiple types
- Flexibility: Easy to add new types without changing existing code
- Maintainability: Changes to specific implementations don't affect client code
- Extensibility: New classes can be added that work with existing polymorphic functions
- Abstraction: Client code works with interfaces rather than concrete implementations