Ereditarietà e polimorfismo in Python: progettare classi con metodi specializzati

by theArchitect
SHARE
Ereditarietà e polimorfismo in Python: progettare classi con metodi specializzati
© Guida-HTML5.it

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

SHARE