Virtual assistance

Python Polymorphism

Polymorphism allows objects of different classes to be treated as objects of a common superclass. Learn method overriding, operator overloading, duck typing, and polymorphism concepts.

Python Polymorphism

What is Polymorphism?

Polymorphism is the ability of an object to take on many forms. In Python, it allows objects of different classes to be treated as objects of a common superclass, enabling the same interface to be used for different underlying forms (data types).

"Polymorphism allows the same operation to behave differently on different classes."

Method Overriding (Runtime Polymorphism)

Child classes can provide their own implementation of parent methods:

class Animal:
    def speak(self):
        return "Some generic animal sound"

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"

class Bird(Animal):
    def speak(self):
        return "Tweet!"

# Polymorphic behavior
def animal_sound(animal):
    return animal.speak()

animals = [Dog(), Cat(), Bird()]

for animal in animals:
    print(animal_sound(animal))

# Output:
# Woof!
# Meow!
# Tweet!

Operator Overloading

Customize operators for user-defined classes:

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)
    
    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
    
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

v1 = Vector(2, 3)
v2 = Vector(1, 4)

print(v1 + v2)    # Vector(3, 7)
print(v1 - v2)    # Vector(1, -1)
print(v1 * 3)     # Vector(6, 9)
print(v1 == v2)   # False

Duck Typing

Python's dynamic typing allows polymorphism without inheritance:

class Dog:
    def speak(self):
        return "Woof!"

class Robot:
    def speak(self):
        return "Beep boop!"

class Alien:
    def speak(self):
        return "Zorg zorg!"

# Duck typing - if it walks like a duck and talks like a duck...
def make_sound(entity):
    return entity.speak()

entities = [Dog(), Robot(), Alien()]

for entity in entities:
    print(make_sound(entity))

# Output:
# Woof!
# Beep boop!
# Zorg zorg!

Function Polymorphism

Built-in functions work with different data types:

# len() works with different types
print(len("hello"))      # 5
print(len([1, 2, 3]))    # 3
print(len({"a": 1, "b": 2}))  # 2

# + operator works differently
print(5 + 3)             # 8 (addition)
print("Hello" + "World") # HelloWorld (concatenation)
print([1, 2] + [3, 4])   # [1, 2, 3, 4] (list concatenation)

Method Overloading (Not Native in Python)

Python doesn't support method overloading like other languages, but we can simulate it:

class Calculator:
    def add(self, *args):
        if len(args) == 2:
            return args[0] + args[1]
        elif len(args) == 3:
            return args[0] + args[1] + args[2]
        else:
            return sum(args)

calc = Calculator()
print(calc.add(1, 2))        # 3
print(calc.add(1, 2, 3))     # 6
print(calc.add(1, 2, 3, 4))  # 10

# Alternative using default parameters
class Calculator2:
    def add(self, a, b=0, c=0):
        return a + b + c

calc2 = Calculator2()
print(calc2.add(1, 2))       # 3
print(calc2.add(1, 2, 3))    # 6

Abstract Base Classes and Polymorphism

Use ABC to define interfaces:

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):
        return 3.14159 * self.radius ** 2
    
    def perimeter(self):
        return 2 * 3.14159 * self.radius

def print_shape_info(shape):
    print(f"Area: {shape.area()}")
    print(f"Perimeter: {shape.perimeter()}")

shapes = [Rectangle(5, 3), Circle(4)]

for shape in shapes:
    print_shape_info(shape)
    print()

Polymorphism with Inheritance

Polymorphism works naturally with inheritance hierarchies:

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
    
    def get_info(self):
        return f"Employee: {self.name}, Salary: ${self.salary}"
    
    def calculate_bonus(self):
        return self.salary * 0.1

class Manager(Employee):
    def __init__(self, name, salary, department):
        super().__init__(name, salary)
        self.department = department
    
    def get_info(self):
        return f"Manager: {self.name}, Department: {self.department}, Salary: ${self.salary}"
    
    def calculate_bonus(self):
        return self.salary * 0.2

class Developer(Employee):
    def __init__(self, name, salary, language):
        super().__init__(name, salary)
        self.language = language
    
    def get_info(self):
        return f"Developer: {self.name}, Language: {self.language}, Salary: ${self.salary}"
    
    def calculate_bonus(self):
        return self.salary * 0.15

def print_employee_details(employee):
    print(employee.get_info())
    print(f"Bonus: ${employee.calculate_bonus()}")

employees = [
    Manager("John", 80000, "IT"),
    Developer("Jane", 70000, "Python"),
    Employee("Bob", 50000)
]

for employee in employees:
    print_employee_details(employee)
    print()

Polymorphism in Data Structures

Collections can hold different object types:

class Car:
    def __init__(self, make, model):
        self.make = make
        self.model = model
    
    def __str__(self):
        return f"{self.make} {self.model}"

class Truck:
    def __init__(self, make, model, capacity):
        self.make = make
        self.model = model
        self.capacity = capacity
    
    def __str__(self):
        return f"{self.make} {self.model} ({self.capacity} tons)"

class Motorcycle:
    def __init__(self, make, model, engine_cc):
        self.make = make
        self.model = model
        self.engine_cc = engine_cc
    
    def __str__(self):
        return f"{self.make} {self.model} ({self.engine_cc}cc)"

# Polymorphic collection
vehicles = [
    Car("Toyota", "Camry"),
    Truck("Ford", "F-150", 2.5),
    Motorcycle("Honda", "CBR", 600)
]

for vehicle in vehicles:
    print(vehicle)

# Polymorphic function
def get_vehicle_type(vehicle):
    if hasattr(vehicle, 'capacity'):
        return "Truck"
    elif hasattr(vehicle, 'engine_cc'):
        return "Motorcycle"
    else:
        return "Car"

for vehicle in vehicles:
    print(f"{vehicle} is a {get_vehicle_type(vehicle)}")

Special Methods for Polymorphism

Implement special methods for polymorphic behavior:

class ComplexNumber:
    def __init__(self, real, imag):
        self.real = real
        self.imag = imag
    
    def __add__(self, other):
        return ComplexNumber(self.real + other.real, self.imag + other.imag)
    
    def __mul__(self, other):
        real = self.real * other.real - self.imag * other.imag
        imag = self.real * other.imag + self.imag * other.real
        return ComplexNumber(real, imag)
    
    def __str__(self):
        return f"{self.real} + {self.imag}i"

class Matrix:
    def __init__(self, data):
        self.data = data
        self.rows = len(data)
        self.cols = len(data[0]) if data else 0
    
    def __add__(self, other):
        if self.rows != other.rows or self.cols != other.cols:
            raise ValueError("Matrices must have same dimensions")
        result = [[self.data[i][j] + other.data[i][j] 
                  for j in range(self.cols)] for i in range(self.rows)]
        return Matrix(result)
    
    def __str__(self):
        return '\n'.join([' '.join(map(str, row)) for row in self.data])

# Both work with + operator
c1 = ComplexNumber(1, 2)
c2 = ComplexNumber(3, 4)
print(c1 + c2)  # 4 + 6i

m1 = Matrix([[1, 2], [3, 4]])
m2 = Matrix([[5, 6], [7, 8]])
print(m1 + m2)
# 6 8
# 10 12

Best Practices

  • Use inheritance for IS-A relationships: Enables natural polymorphism
  • Leverage duck typing: Don't force inheritance when not needed
  • Implement consistent interfaces: Same method names across classes
  • Use abstract base classes: Define contracts for polymorphism
  • Override special methods appropriately: For operator overloading
  • Document polymorphic behavior: Use docstrings
  • Test polymorphic code thoroughly: Different implementations
  • Use isinstance() sparingly: Prefer duck typing
  • Keep method signatures consistent: Across polymorphic classes
  • Use polymorphism for extensibility: Easy to add new types

Common Patterns

# Strategy Pattern
class SortStrategy:
    def sort(self, data):
        pass

class BubbleSort(SortStrategy):
    def sort(self, data):
        return sorted(data, reverse=True)  # Descending

class QuickSort(SortStrategy):
    def sort(self, data):
        return sorted(data)  # Ascending

class Sorter:
    def __init__(self, strategy):
        self.strategy = strategy
    
    def sort(self, data):
        return self.strategy.sort(data)

# Command Pattern
class Command:
    def execute(self):
        pass

class SaveCommand(Command):
    def execute(self):
        print("Saving document...")

class PrintCommand(Command):
    def execute(self):
        print("Printing document...")

class MenuItem:
    def __init__(self, command):
        self.command = command
    
    def click(self):
        self.command.execute()

# Observer Pattern
class Observer:
    def update(self, message):
        pass

class EmailNotifier(Observer):
    def update(self, message):
        print(f"Email: {message}")

class SMSNotifier(Observer):
    def update(self, message):
        print(f"SMS: {message}")

class Subject:
    def __init__(self):
        self.observers = []
    
    def attach(self, observer):
        self.observers.append(observer)
    
    def notify(self, message):
        for observer in self.observers:
            observer.update(message)

# Usage
subject = Subject()
subject.attach(EmailNotifier())
subject.attach(SMSNotifier())
subject.notify("System update available!")

Polymorphism is a powerful concept that enables flexible and extensible code. It allows you to write generic code that works with different types of objects, making your programs more maintainable and reusable.