Introduzione
In Python, ereditarietà e polimorfismo sono due concetti fondamentali per costruire software estendibile e leggibile. Quando li usiamo bene, possiamo definire una base comune e poi specializzare il comportamento delle classi derivate senza duplicare codice.
In questo tutorial vedremo un sotto-argomento molto pratico: come progettare una gerarchia di classi con metodi specializzati per gestire processi diversi in modo uniforme. L’idea è utile in molti contesti reali: esportazione dati, notifiche, pagamenti, elaborazione file, integrazione con API e molto altro.
L’obiettivo non è solo creare classi figlie, ma far sì che tutte espongano la stessa “interfaccia comportamentale”, così da poterle usare in modo polimorfico. In pratica: il codice che usa gli oggetti non deve sapere quale classe concreta sta gestendo.
Codice completo
from abc import ABC, abstractmethod
from datetime import datetime
class ReportExporter(ABC):
"""
Classe base astratta per esportare report in formati diversi.
Definisce il flusso comune e lascia alle sottoclassi i dettagli specifici.
"""
def __init__(self, report_name: str):
self.report_name = report_name
def export(self, data: list[dict]) -> str:
"""
Metodo pubblico comune:
1. prepara i dati
2. genera il contenuto
3. salva/esporta il risultato
"""
cleaned_data = self._validate_and_prepare(data)
content = self._build_content(cleaned_data)
filename = self._build_filename()
self._save(filename, content)
return filename
def _validate_and_prepare(self, data: list[dict]) -> list[dict]:
"""
Logica condivisa: controlla che i dati non siano vuoti
e che ogni record abbia almeno un campo ´name´.
"""
if not data:
raise ValueError("Nessun dato da esportare.")
prepared = []
for item in data:
if "name" not in item:
raise ValueError("Ogni record deve contenere il campo ´name´.")
prepared.append(item)
return prepared
def _build_filename(self) -> str:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
return f"{self.report_name}_{timestamp}.{self.extension}"
def _save(self, filename: str, content: str) -> None:
"""
In un progetto reale qui scriveremmo su file o invieremmo su storage.
Per semplicità stampiamo il risultato.
"""
print(f"n--- Salvataggio: {filename} ---")
print(content)
@property
@abstractmethod
def extension(self) -> str:
"""Estensione del file, definita dalle classi figlie."""
raise NotImplementedError
@abstractmethod
def _build_content(self, data: list[dict]) -> str:
"""Costruzione del contenuto specifico del formato."""
raise NotImplementedError
class CsvExporter(ReportExporter):
@property
def extension(self) -> str:
return "csv"
def _build_content(self, data: list[dict]) -> str:
headers = ["name", "email", "country"]
lines = [",".join(headers)]
for item in data:
row = [
str(item.get("name", "")),
str(item.get("email", "")),
str(item.get("country", "")),
]
lines.append(",".join(row))
return "n".join(lines)
class JsonExporter(ReportExporter):
@property
def extension(self) -> str:
return "json"
def _build_content(self, data: list[dict]) -> str:
import json
return json.dumps(data, indent=2, ensure_ascii=False)
class MarkdownExporter(ReportExporter):
@property
def extension(self) -> str:
return "md"
def _build_content(self, data: list[dict]) -> str:
lines = ["# Report esportato", ""]
for item in data:
lines.append(f"- **Nome**: {item.get(´name´, ´´)}")
lines.append(f" - Email: {item.get(´email´, ´N/D´)}")
lines.append(f" - Paese: {item.get(´country´, ´N/D´)}")
lines.append("")
return "n".join(lines)
def run_export(exporter: ReportExporter, data: list[dict]) -> str:
"""
Funzione polimorfica: accetta qualsiasi exporter che rispetti il contratto.
"""
return exporter.export(data)
if __name__ == "__main__":
sample_data = [
{"name": "Alice", "email": "[email protected]", "country": "IT"},
{"name": "Marco", "email": "[email protected]", "country": "ES"},
{"name": "Sara", "email": "[email protected]", "country": "FR"},
]
exporters = [
CsvExporter("clienti"),
JsonExporter("clienti"),
MarkdownExporter("clienti"),
]
for exporter in exporters:
run_export(exporter, sample_data)
Spiegazione
L’esempio mostra una gerarchia di classi per esportare un report in formati diversi. La classe base ReportExporter definisce la parte comune del processo: validazione, costruzione del nome file e salvataggio. Le classi figlie, invece, specializzano solo la parte che cambia: il formato del contenuto e l’estensione del file.
1. La classe base come contratto e riuso del flusso
ReportExporter è una classe astratta. Questo significa che non vogliamo istanziarla direttamente, ma usarla come base concettuale. Contiene il metodo export(), che rappresenta il flusso principale. Questo è un ottimo esempio di ereditarietà ben usata: la logica comune resta in un solo punto, evitando duplicazione.
2. Metodi astratti per forzare la specializzazione
I metodi _build_content() ed extension sono astratti. Ogni sottoclasse deve implementarli, altrimenti Python segnala un errore. Questo è molto utile perché impedisce di creare classi incomplete.
3. Polimorfismo nel punto di utilizzo
La funzione run_export() accetta un parametro di tipo ReportExporter, ma in realtà può lavorare con qualsiasi oggetto derivato che rispetti il contratto. Qui entra in gioco il polimorfismo: il codice chiamante non deve sapere se sta usando un exporter CSV, JSON o Markdown. Ogni oggetto risponde al metodo export() in modo diverso, ma con la stessa interfaccia.
4. Perché questo approccio è utile
Immagina di dover aggiungere un nuovo formato, ad esempio XML. Con questa struttura ti basta creare una nuova sottoclasse e implementare i dettagli specifici. Non devi riscrivere la validazione, la generazione del nome file o il flusso generale. Questo rende il codice più manutenibile e più facile da estendere.
Best practice
- Usa la classe base per condividere il flusso, non per accumulare tutto il codice. La base dovrebbe contenere la logica comune; le differenze vanno nelle sottoclassi.
- Preferisci metodi astratti quando una specializzazione è obbligatoria. In questo modo eviti classi figlie incomplete o comportamenti ambigui.
- Nomina chiaramente i metodi protetti con il prefisso underscore. Metodi come
_build_content()indicano che sono pensati per l’uso interno della gerarchia. - Riduci la logica duplicata. Se due sottoclassi condividono una parte del comportamento, valuta di spostarla nella classe base.
- Progetta pensando al polimorfismo. Se più classi devono essere usate nello stesso punto del codice, assicurati che espongano metodi coerenti.
- Non abusare dell’ereditarietà. Se le classi non condividono davvero una relazione “è un”, forse è meglio usare composizione.
- Scrivi classi piccole e focalizzate. Una gerarchia semplice è più facile da testare e da estendere rispetto a una struttura troppo profonda.
Riepilogo
In questo tutorial abbiamo visto come usare ereditarietà e polimorfismo per progettare una famiglia di classi specializzate, mantenendo un flusso comune nella classe base. Il risultato è un codice più pulito, riusabile ed estendibile.
Il punto chiave è questo: la classe base definisce cosa deve accadere, le classi figlie definiscono come accade nei dettagli. Grazie al polimorfismo, il codice che usa questi oggetti resta semplice e indipendente dall’implementazione concreta.
Approfondisci con risorse ufficiali
- Documentazione ufficiale Python sulle classi:
https://docs.python.org/3/tutorial/classes.html
- ABC - Abstract Base Classes:
https://docs.python.org/3/library/abc.html
- Tipi e annotazioni in Python:
https://docs.python.org/3/library/typing.html
- Data model di Python:
https://docs.python.org/3/reference/datamodel.html
