Polimorfismo in Python
Scopri il polimorfismo Python: override dei metodi, duck typing, overloading degli operatori e interfacce astratte, con esempi chiari.
Il polimorfismo (dal greco "molte forme") consente a un singolo pezzo di codice di funzionare con oggetti di tipi diversi, purché tali oggetti supportino l'interfaccia attesa. Si chiama lo stesso nome di metodo su ciascun oggetto, e ogni oggetto risponde secondo la propria implementazione.
Il polimorfismo è uno dei quattro pilastri della programmazione orientata agli oggetti, insieme a encapsulation, inheritance e abstraction. È ciò che fa sì che le funzioni che accettano un tipo di classe base funzionino automaticamente con qualsiasi sottoclasse.
Questo capitolo tratta:
- Cosa è il polimorfismo e perché è importante
- Il polimorfismo attraverso l'override dei metodi
- Il polimorfismo attraverso il duck typing
- L'overloading degli operatori — una forma di polimorfismo integrata in Python
- Le classi base astratte come modo formale per definire interfacce polimorfiche
- Pattern pratici e insidie comuni
Prima di leggere questo capitolo, assicurati di avere familiarità con le classi e gli oggetti Python e l'ereditarietà Python.
Perché il Polimorfismo è Importante
Senza polimorfismo, una funzione che lavora con gli animali richiederebbe una catena esplicita di if/elif per ogni tipo di animale:
def make_sound(animal):
if type(animal).__name__ == "Dog":
print("Woof!")
elif type(animal).__name__ == "Cat":
print("Meow!")
elif type(animal).__name__ == "Bird":
print("Tweet!")
# ... add a new branch every time you add a new animal typeQuesto è fragile. Ogni nuovo tipo di animale richiede la modifica di questa funzione. Con il polimorfismo si scrive:
def make_sound(animal):
animal.speak() # works for any object that has a speak() methodAggiungere un nuovo tipo di animale richiede solo di definire il suo metodo speak() — la funzione stessa non cambia mai. Questo è il principio open/closed: aperto alle estensioni, chiuso alle modifiche.
Polimorfismo Attraverso l'Override dei Metodi
La forma più comune di polimorfismo in Python è il method overriding: una sottoclasse fornisce la propria versione di un metodo definito dalla classe genitore.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound."
class Dog(Animal):
def speak(self):
return f"{self.name} says woof!"
class Cat(Animal):
def speak(self):
return f"{self.name} says meow!"
class Bird(Animal):
def speak(self):
return f"{self.name} says tweet!"Ora un singolo ciclo funziona con tutti e tre i tipi:
animals = [Dog("Rex"), Cat("Whiskers"), Bird("Tweety")]
for animal in animals:
print(animal.speak())
# Rex says woof!
# Whiskers says meow!
# Tweety says tweet!animal.speak() viene indirizzato alla versione corretta a runtime in base al tipo effettivo dell'oggetto. Questo meccanismo di dispatch a runtime è chiamato dynamic dispatch o late binding.
Estendere Versus Sostituire il Metodo Genitore
Quando si effettua l'override, si può sostituire completamente il comportamento del genitore oppure estenderlo usando super():
class Animal:
def speak(self):
print("[Animal vocalization]")
class Dog(Animal):
def speak(self):
super().speak() # keep the parent's output
print("Woof! (Dog override adds this)")
Dog().speak()
# [Animal vocalization]
# Woof! (Dog override adds this)Usa super() quando la versione del genitore esegue configurazioni o logging utili che dovrebbero comunque essere eseguiti. Omettilo quando vuoi sostituire completamente il comportamento.
Polimorfismo Attraverso il Duck Typing
Il sistema dei tipi in Python è strutturale piuttosto che nominale. Un oggetto non deve necessariamente appartenere a una particolare gerarchia di classi; ha solo bisogno di avere i metodi giusti. Questo si chiama duck typing — dal detto "se cammina come un'anatra e starnazza come un'anatra, allora è un'anatra."
class Dog:
def speak(self):
return "Woof!"
class Robot:
def speak(self):
return "Beep boop."
class Human:
def speak(self):
return "Hello!"
def introduce(entity):
print(entity.speak())
introduce(Dog()) # Woof!
introduce(Robot()) # Beep boop.
introduce(Human()) # Hello!Dog, Robot e Human non hanno alcuna classe genitore comune (a parte il built-in object). Eppure introduce() funziona con tutti e tre perché ognuno ha un metodo speak(). La funzione non verifica il tipo di entity — chiama semplicemente il metodo e si affida al fatto che l'oggetto risponderà correttamente.
Quando Usare Duck Typing vs. Ereditarietà
| Situazione | Approccio preferito |
|---|---|
| Gli oggetti sono logicamente correlati (tutti animali) | Gerarchia di ereditarietà |
| Gli oggetti non sono correlati ma condividono un comportamento | Duck typing |
| Si vuole imporre l'interfaccia al momento della definizione | Classi base astratte |
| Si lavora con tipi built-in o classi di terze parti che non si possono modificare | Duck typing |
Il duck typing è idiomatico in Python e viene usato ampiamente nella libreria standard — ad esempio, len() funziona su qualsiasi oggetto che definisce __len__, indipendentemente dalla sua classe.
Polimorfismo con Funzioni e Cicli
Si può scrivere una singola funzione che tratta uniformemente tipi diversi attraverso il polimorfismo. Consideriamo un'applicazione di disegno:
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
import math
return math.pi * self.radius ** 2
def describe(self):
return f"Circle with radius {self.radius}"
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def describe(self):
return f"Rectangle {self.width}x{self.height}"
class Triangle:
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return 0.5 * self.base * self.height
def describe(self):
return f"Triangle base={self.base} height={self.height}"
shapes = [Circle(5), Rectangle(4, 6), Triangle(3, 8)]
for shape in shapes:
print(f"{shape.describe()}: area = {shape.area():.2f}")
# Circle with radius 5: area = 78.54
# Rectangle 4x6: area = 24.00
# Triangle base=3 height=8: area = 12.00Il ciclo chiama area() e describe() su ogni forma senza preoccuparsi di quale classe appartenga l'oggetto. Aggiungere successivamente una classe Pentagon richiede solo di scrivere la nuova classe — il ciclo non cambia.
Overloading degli Operatori
Anche gli operatori aritmetici e di confronto in Python sono polimorfici. + sugli interi somma numeri; + sulle stringhe le concatena; + sulle liste le unisce. Python ottiene questo grazie ai metodi speciali (dunder).
È possibile far rispondere le proprie classi agli operatori definendo questi metodi:
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 __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v1 * 3) # Vector(3, 6)Lo stesso operatore + si comporta ora diversamente a seconda che gli operandi siano interi, stringhe o oggetti Vector. Questo è il polimorfismo a livello di operatore.
Per un approfondimento sui metodi speciali di Python, vedi Python Magic Methods.
Polimorfismo con Classi Base Astratte
Le classi base astratte (ABC) portano il duck typing un passo avanti imponendo l'interfaccia al momento della definizione della classe. Se una sottoclasse non implementa un metodo obbligatorio, Python solleva un TypeError nel momento in cui si cerca di istanziarla.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
"""Return the area of the shape."""
@abstractmethod
def perimeter(self) -> float:
"""Return the perimeter of the shape."""
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
import math
return math.pi * self.radius ** 2
def perimeter(self) -> float:
import math
return 2 * math.pi * self.radius
class Square(Shape):
def __init__(self, side: float):
self.side = side
def area(self) -> float:
return self.side ** 2
def perimeter(self) -> float:
return 4 * self.side
def print_info(shape: Shape) -> None:
print(f"Area: {shape.area():.2f}")
print(f"Perimeter: {shape.perimeter():.2f}")
print_info(Circle(5))
# Area: 78.54
# Perimeter: 31.42
print_info(Square(4))
# Area: 16.00
# Perimeter: 16.00Se si tenta di istanziare una sottoclasse che non implementa area():
class Blob(Shape):
pass # forgot to implement area() and perimeter()
b = Blob()
# TypeError: Can't instantiate abstract class Blob with abstract methods area, perimeterLe ABC forniscono la rete di sicurezza di un contratto formale pur consentendo il polimorfismo a runtime.
ABC vs. Duck Typing — Quale Usare?
- Il duck typing è più semplice e flessibile. Preferirlo per codebase interne di piccole dimensioni e script.
- Le ABC rendono esplicita l'interfaccia attesa, intercettano precocemente i bug dovuti a metodi mancanti e compaiono nell'auto-completamento dell'IDE e nei type checker. Preferirle in codebase più grandi, librerie pubbliche e ovunque si voglia imporre un contratto.
Un Esempio Reale: Elaborazione dei Pagamenti
Il polimorfismo brilla nei design di tipo plugin. Consideriamo un sistema di pagamento che deve supportare più provider:
from abc import ABC, abstractmethod
class PaymentProvider(ABC):
@abstractmethod
def charge(self, amount: float, currency: str) -> bool:
"""Attempt to charge the given amount. Return True on success."""
@abstractmethod
def refund(self, transaction_id: str) -> bool:
"""Refund a previous transaction. Return True on success."""
class StripeProvider(PaymentProvider):
def charge(self, amount: float, currency: str) -> bool:
print(f"[Stripe] Charged {amount} {currency}")
return True
def refund(self, transaction_id: str) -> bool:
print(f"[Stripe] Refunded transaction {transaction_id}")
return True
class PayPalProvider(PaymentProvider):
def charge(self, amount: float, currency: str) -> bool:
print(f"[PayPal] Charged {amount} {currency}")
return True
def refund(self, transaction_id: str) -> bool:
print(f"[PayPal] Refunded transaction {transaction_id}")
return True
def process_order(provider: PaymentProvider, amount: float) -> None:
success = provider.charge(amount, "USD")
if success:
print("Order complete.")
process_order(StripeProvider(), 99.99)
# [Stripe] Charged 99.99 USD
# Order complete.
process_order(PayPalProvider(), 49.5)
# [PayPal] Charged 49.5 USD
# Order complete.process_order non sa né si preoccupa se riceve un StripeProvider o un PayPalProvider. Aggiungere un nuovo CryptoProvider richiede solo di scrivere la nuova classe — nient'altro cambia. Questo è il polimorfismo che garantisce estensibilità nel mondo reale.
Insidie Comuni
Insidia 1: Verificare i Tipi con type() Invece di isinstance()
Confrontare type(obj) == Dog vanifica il polimorfismo perché restituisce False per le sottoclassi. Preferire isinstance(obj, Animal), che restituisce True sia per Dog che per qualsiasi futura sottoclasse:
class Animal:
pass
class Dog(Animal):
pass
d = Dog()
# Fragile — breaks for subclasses:
print(type(d) == Animal) # False
# Correct — subclass-aware:
print(isinstance(d, Animal)) # TrueInsidia 2: Firme dei Metodi Non Coerenti
Il polimorfismo presuppone che tutte le implementazioni di un metodo accettino gli stessi argomenti. Se Dog.speak() richiede un argomento che Cat.speak() non richiede, i chiamanti che li trattano uniformemente andranno in errore:
# Inconsistent — will cause errors in a loop
class Dog:
def speak(self, volume): # extra argument!
return f"Woof at volume {volume}"
class Cat:
def speak(self):
return "Meow!"Mantenere firme dei metodi coerenti tra le classi polimorfiche.
Insidia 3: Argomenti Default Mutabili nei Metodi Override
Questa è un'insidia più generale di Python, ma si presenta spesso nelle gerarchie di classi: non usare mai un default mutabile (lista, dizionario) come valore di argomento predefinito — viene creato una sola volta e condiviso tra tutte le chiamate.
# Bug: the list is shared across all instances
class Item:
def __init__(self, tags=[]): # BAD
self.tags = tags
# Fix:
class Item:
def __init__(self, tags=None):
self.tags = tags if tags is not None else []Riepilogo
| Concetto | Significato |
|---|---|
| Method overriding | Una sottoclasse fornisce la propria versione di un metodo genitore |
| Dynamic dispatch | Python sceglie la versione corretta del metodo a runtime |
| Duck typing | Qualsiasi oggetto con i metodi giusti funziona, indipendentemente dalla sua classe |
| Operator overloading | I metodi dunder (__add__, __len__, …) rendono polimorfici gli operatori |
| Abstract base classes | Impongono formalmente l'interfaccia che le sottoclassi devono implementare |
Il polimorfismo è ciò che rende il codice estensibile senza modifiche. Scrivi funzioni che dipendono dal comportamento (nomi dei metodi), non dai tipi concreti, e il tuo codice accoglierà naturalmente nuove classi senza bisogno di cambiamenti.