Ereditarietà e polimorfismo in Python: un registro automatico di plugin

by theArchitect
SHARE
Ereditarietà e polimorfismo in Python: un registro automatico di plugin
© Guida-HTML5.it

Introduzione

In Python, l’ereditarietà permette a una classe di riutilizzare e specializzare il comportamento definito da un’altra classe. Il polimorfismo, invece, consente di utilizzare oggetti diversi attraverso la stessa interfaccia: il codice chiamante invoca un metodo comune senza dover conoscere la classe concreta dell’oggetto.

In questo tutorial useremo questi concetti per costruire un piccolo sistema di plugin per generare report. Ogni formato di report sarà rappresentato da una sottoclasse diversa, per esempio HTML, testo semplice o JSON. Il programma principale potrà selezionare il formato richiesto senza contenere istruzioni specifiche per ogni tipo di report.

L’aspetto pratico dell’esempio è un registro automatico delle sottoclassi. Quando definiamo una nuova classe di report, questa viene registrata automaticamente e può essere recuperata usando un nome. In questo modo possiamo aggiungere nuovi formati senza modificare la logica principale dell’applicazione.

Codice completo

import json


class ReportPlugin:
    """Classe base per tutti i generatori di report."""

    _registry = {}

    def __init_subclass__(cls, *, formato=None, **kwargs):
        """
        Viene eseguito automaticamente quando nasce una sottoclasse.

        Il parametro ´formato´ identifica il plugin nel registro.
        """
        super().__init_subclass__(**kwargs)

        if formato is None:
            raise ValueError(
                f"La classe {cls.__name__} deve specificare un formato"
            )

        if formato in ReportPlugin._registry:
            raise ValueError(
                f"Esiste già un plugin per il formato {formato!r}"
            )

        ReportPlugin._registry[formato] = cls

    def genera(self, dati):
        """
        Metodo comune a tutti i plugin.

        Le sottoclassi devono fornire la propria implementazione.
        """
        raise NotImplementedError(
            "Ogni plugin deve implementare il metodo genera()"
        )

    @classmethod
    def crea(cls, formato):
        """Restituisce un´istanza del plugin richiesto."""
        try:
            plugin_class = cls._registry[formato]
        except KeyError:
            formati = ", ".join(sorted(cls._registry))
            raise ValueError(
                f"Formato {formato!r} non disponibile. "
                f"Formati validi: {formati}"
            )

        return plugin_class()

    @classmethod
    def formati_disponibili(cls):
        """Restituisce i formati registrati."""
        return sorted(cls._registry)


class ReportTesto(ReportPlugin, formato="testo"):
    def genera(self, dati):
        righe = ["REPORT VENDITE", "=" * 30]

        for prodotto, quantita in dati.items():
            righe.append(f"{prodotto}: {quantita} unità")

        return "n".join(righe)


class ReportJson(ReportPlugin, formato="json"):
    def genera(self, dati):
        return json.dumps(
            {
                "tipo": "vendite",
                "prodotti": dati
            },
            indent=2,
            ensure_ascii=False
        )


class ReportHtml(ReportPlugin, formato="html"):
    def genera(self, dati):
        righe = [
            "<h1>Report vendite</h1>",
            "<ul>"
        ]

        for prodotto, quantita in dati.items():
            righe.append(
                f"  <li>{prodotto}: {quantita} unità</li>"
            )

        righe.append("</ul>")
        return "n".join(righe)


def esporta_report(formato, dati):
    """
    Funzione indipendente dalle classi concrete.

    Usa sempre lo stesso metodo genera(), qualunque sia il plugin.
    """
    plugin = ReportPlugin.crea(formato)
    return plugin.genera(dati)


def main():
    dati = {
        "Tastiera": 12,
        "Mouse": 25,
        "Monitor": 7
    }

    print("Formati disponibili:")
    print(ReportPlugin.formati_disponibili())

    for formato in ReportPlugin.formati_disponibili():
        print(f"n--- Report {formato} ---")
        print(esporta_report(formato, dati))


if __name__ == "__main__":
    main()

Spiegazione

La classe base

ReportPlugin è la classe base della gerarchia. Definisce il contratto minimo che ogni plugin deve rispettare: deve possedere un metodo genera(dati). Il metodo nella classe base solleva NotImplementedError, perché non esiste un formato generico valido per tutti i report.

Le sottoclassi ReportTesto, ReportJson e ReportHtml ereditano da ReportPlugin e forniscono implementazioni diverse dello stesso metodo. Questo è il punto centrale del polimorfismo: la funzione esporta_report() chiama sempre plugin.genera(dati), senza controllare se il plugin sia testuale, JSON o HTML.

Registrazione automatica con __init_subclass__

__init_subclass__() è un metodo speciale chiamato automaticamente da Python ogni volta che viene definita una sottoclasse. Qui lo utilizziamo per inserire la classe nel dizionario _registry.

La dichiarazione:

class ReportJson(ReportPlugin, formato="json"):

registra automaticamente ReportJson con la chiave "json". Non è necessario aggiungere manualmente la classe a una lista o a un dizionario. Se due plugin usassero lo stesso formato, il codice solleverebbe un errore immediato, evitando configurazioni ambigue.

Factory e selezione dinamica

Il metodo di classe crea() funziona come una semplice factory. Riceve il nome del formato, cerca la classe corrispondente nel registro e ne crea un’istanza.

Il chiamante non deve scrivere codice come:

if formato == "json":
    plugin = ReportJson()
elif formato == "html":
    plugin = ReportHtml()

Questa soluzione diventerebbe difficile da mantenere con molti formati. Grazie al registro, aggiungere un nuovo plugin richiede soltanto la definizione di una nuova sottoclasse:

class ReportCsv(ReportPlugin, formato="csv"):
    def genera(self, dati):
        righe = ["prodotto,quantita"]

        for prodotto, quantita in dati.items():
            righe.append(f"{prodotto},{quantita}")

        return "n".join(righe)

La nuova classe sarà immediatamente disponibile tramite ReportPlugin.crea("csv"), senza modificare esporta_report().

Best practice

  • Definisci un contratto chiaro: stabilisci quali metodi e quali dati devono essere supportati da tutte le sottoclassi.
  • Mantieni la classe base essenziale: inserisci solo comportamento realmente comune. Non trasformarla in un contenitore di funzionalità non correlate.
  • Preferisci il polimorfismo ai controlli sul tipo: evita sequenze di isinstance() quando puoi invocare direttamente un metodo comune.
  • Valida le configurazioni presto: controllare duplicati e formati mancanti durante la definizione delle classi rende gli errori più facili da individuare.
  • Separa selezione ed esecuzione: la factory sceglie l’oggetto, mentre il plugin si occupa di generare il risultato.
  • Progetta plugin testabili: ogni metodo genera() dovrebbe poter essere verificato con dati piccoli e prevedibili, senza dipendere da file o servizi esterni.
  • Usa nomi stabili per il registro: il valore "json" è parte della configurazione dell’applicazione e dovrebbe essere documentato.

Riepilogo

L’ereditarietà ci ha permesso di definire una famiglia di generatori partendo da una classe base comune. Il polimorfismo ha reso possibile usare tutti i generatori tramite il metodo genera(), indipendentemente dal loro formato concreto.

Con __init_subclass__() abbiamo aggiunto un meccanismo di registrazione automatica. Associato a una factory, questo approccio è utile per sistemi estendibili: esportatori, parser, driver, strategie di calcolo e componenti configurabili.

Il vantaggio principale non è soltanto ridurre il numero di righe, ma mantenere la logica principale stabile mentre nuove funzionalità vengono aggiunte tramite nuove sottoclassi.

Approfondisci con risorse ufficiali

  • Documentazione ufficiale sulle classi Python: docs.python.org/3/tutorial/classes.html
  • Riferimento a __init_subclass__: docs.python.org/3/reference/datamodel.html#object.__init_subclass__
  • Documentazione sul modulo json: docs.python.org/3/library/json.html
  • Guida ufficiale alla gestione delle eccezioni: docs.python.org/3/tutorial/errors.html

SHARE