Introduzione
In Python, ereditarietà e polimorfismo sono due concetti fondamentali per costruire codice organizzato, estendibile e facile da mantenere. Molti tutorial si concentrano solo su classi base, classi astratte o override dei metodi, ma c’è un aspetto molto pratico che spesso viene trascurato: la progettazione di oggetti che condividono una stessa “interfaccia d’uso” pur avendo implementazioni diverse.
Questo approccio è utile in scenari reali come:
- gestione di diversi sistemi di pagamento;
- invio di notifiche via email, SMS o webhook;
- esportazione di report in formati differenti;
- integrazione con servizi esterni con comportamenti simili ma logiche diverse.
L’idea è semplice: definire una classe base che descrive il comportamento atteso, e poi specializzarla in più classi concrete. In questo modo, il codice che usa gli oggetti non deve sapere quale classe concreta sta usando, ma solo quali metodi può chiamare. Questo è il cuore del polimorfismo.
Codice completo
from abc import ABC, abstractmethod
class Notifica(ABC):
"""Classe base astratta: definisce il contratto comune."""
def __init__(self, destinatario: str):
self.destinatario = destinatario
@abstractmethod
def invia(self, messaggio: str) -> str:
"""Ogni sottoclasse deve implementare questo metodo."""
pass
class EmailNotifica(Notifica):
def invia(self, messaggio: str) -> str:
return f"Email inviata a {self.destinatario}: {messaggio}"
class SmsNotifica(Notifica):
def invia(self, messaggio: str) -> str:
return f"SMS inviato a {self.destinatario}: {messaggio}"
class WebhookNotifica(Notifica):
def __init__(self, destinatario: str, endpoint: str):
super().__init__(destinatario)
self.endpoint = endpoint
def invia(self, messaggio: str) -> str:
return f"Webhook inviato a {self.endpoint} per {self.destinatario}: {messaggio}"
def processa_notifiche(notifiche: list[Notifica], messaggio: str) -> None:
"""Funzione polimorfica: lavora con qualunque sottoclasse di Notifica."""
for notifica in notifiche:
risultato = notifica.invia(messaggio)
print(risultato)
if __name__ == "__main__":
notifiche = [
EmailNotifica("[email protected]"),
SmsNotifica("+39 333 1234567"),
WebhookNotifica("servizio-interno", "https://api.example.com/webhook"),
]
processa_notifiche(notifiche, "Backup completato con successo") Spiegazione
Nel codice sopra abbiamo costruito una piccola gerarchia di classi per gestire le notifiche. La classe Notifica rappresenta il comportamento comune: tutte le notifiche hanno un destinatario e tutte devono saper inviare un messaggio.
1. La classe base astratta
Notifica eredita da ABC, cioè Abstract Base Class. Questo serve a dichiarare che la classe non è pensata per essere istanziata direttamente, ma per essere estesa.
Il metodo invia() è decorato con @abstractmethod. Questo obbliga ogni sottoclasse a fornire la propria implementazione. È una forma di “contratto”: se una classe figlia non implementa il metodo, Python impedirà la creazione dell’oggetto.
2. Le classi concrete
EmailNotifica, SmsNotifica e WebhookNotifica sono tre implementazioni diverse dello stesso comportamento. Ciascuna personalizza il modo in cui il messaggio viene inviato.
- EmailNotifica restituisce un testo che simula l’invio di una email.
- SmsNotifica simula l’invio di un SMS.
- WebhookNotifica aggiunge un attributo extra,
endpoint, utile solo per questo tipo di notifica.
Qui si vede bene l’ereditarietà: le sottoclassi riusano l’attributo destinatario definito nella classe base. Nello specifico, WebhookNotifica usa anche super().__init__(destinatario) per inizializzare correttamente la parte ereditata.
3. Il polimorfismo nella funzione processa_notifiche
La funzione processa_notifiche() riceve una lista di oggetti di tipo Notifica. In realtà, può lavorare con qualsiasi sottoclasse: l’unico requisito è che esista il metodo invia().
Questo è polimorfismo: lo stesso codice funziona con oggetti diversi, purché condividano lo stesso comportamento pubblico.
Il vantaggio è enorme: se domani aggiungi PushNotifica o TelegramNotifica, non devi modificare la funzione di elaborazione. Ti basta creare una nuova classe che implementi invia().
4. Perché questo approccio è utile
Questo stile di progettazione riduce l’accoppiamento tra le parti del programma. La logica che usa le notifiche non deve conoscere i dettagli interni di ogni canale di invio. Ogni classe si occupa del proprio comportamento, mentre il resto del sistema lavora con un’interfaccia comune.
Best practice
- Usa classi base astratte quando vuoi imporre un contratto. Se tutte le sottoclassi devono implementare un metodo,
ABCe@abstractmethodsono la scelta più chiara. - Progetta metodi con nomi coerenti. Se la classe base definisce
invia(), tutte le sottoclassi devono usare lo stesso nome. Il polimorfismo funziona meglio quando l’API è uniforme. - Evita gerarchie troppo profonde. L’ereditarietà è potente, ma una struttura con troppi livelli diventa difficile da capire e mantenere.
- Usa
super()per inizializzare la parte comune. Questo riduce duplicazione e rende il codice più robusto se la classe base cambia. - Non forzare l’ereditarietà se basta la composizione. Se le classi condividono solo un comportamento isolato, a volte è meglio usare oggetti separati invece di una gerarchia artificiale.
- Annota i tipi quando possibile. Le type hint aiutano a capire il contratto tra classi e rendono il codice più leggibile.
- Fai attenzione ai metodi che cambiano il significato nei figli. Se una sottoclasse stravolge troppo il comportamento della base, potresti aver modellato male la gerarchia.
Riepilogo
Ereditarietà e polimorfismo in Python non servono solo a “riusare codice”, ma soprattutto a progettare sistemi flessibili. In questo tutorial abbiamo visto un caso pratico: una famiglia di classi per gestire notifiche con lo stesso metodo pubblico invia(), ma con implementazioni diverse.
Il punto chiave è che il codice client lavora con un concetto astratto, non con i dettagli concreti. Questo rende il programma più facile da estendere, testare e mantenere.
- Ereditarietà: le sottoclassi riusano attributi e struttura della classe base.
- Polimorfismo: oggetti diversi possono essere trattati nello stesso modo se espongono gli stessi metodi.
- Classi astratte: aiutano a imporre regole chiare alle sottoclassi.
- Progettazione pulita: meno duplicazione, più estendibilità.
Approfondisci con risorse ufficiali
- Documentazione ufficiale Python su
abc: https://docs.python.org/3/library/abc.html - Documentazione ufficiale Python sulle classi: https://docs.python.org/3/tutorial/classes.html
- Documentazione ufficiale Python su
super(): https://docs.python.org/3/library/functions.html#super - PEP 3119 - Introduzione alle classi astratte in Python: https://peps.python.org/pep-3119/
