Python: ereditarietà e polimorfismo per modellare comportamenti intercambiabili

by theArchitect
SHARE
Python: ereditarietà e polimorfismo per modellare comportamenti intercambiabili
© Guida-HTML5.it

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, ABC e @abstractmethod sono 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

SHARE