Decoratori e metaprogrammazione: creare registri automatici di funzioni in Python

by theArchitect
SHARE
Decoratori e metaprogrammazione: creare registri automatici di funzioni in Python
© Guida-HTML5.it

Introduzione

Quando si parla di decoratori in Python, spesso si pensa a logging, caching o validazione. In realtà, uno degli usi più interessanti e pratici è la registrazione automatica di funzioni in un “catalogo” centrale. Questo approccio è molto utile in applicazioni reali, ad esempio per costruire:

  • un sistema di comandi da riga di comando;
  • un motore di plugin;
  • un router semplice per gestire azioni o eventi;
  • un dispatcher di task in un backend.

Il vantaggio è che non devi aggiornare manualmente una lista di funzioni ogni volta che ne aggiungi una nuova: il decoratore si occupa di registrarla automaticamente. Questo è un esempio concreto di metaprogrammazione, cioè codice che modifica o organizza altri elementi del programma in fase di esecuzione.

In questo tutorial vedremo come costruire un piccolo registro di comandi usando un decoratore personalizzato. L’obiettivo è capire non solo come funziona, ma anche perché è utile in progetti reali.

Codice completo

from functools import wraps

# Registro globale dei comandi
COMMANDS = {}

def register_command(name=None):
    """
    Decoratore parametrico che registra una funzione in un dizionario globale.
    Se name non viene passato, usa il nome della funzione.
    """
    def decorator(func):
        command_name = name or func.__name__

        if command_name in COMMANDS:
            raise ValueError(f"Comando già registrato: {command_name}")

        COMMANDS[command_name] = func

        @wraps(func)
        def wrapper(*args, **kwargs):
            # Qui potremmo aggiungere logica extra, ma in questo esempio
            # il decoratore si occupa soprattutto della registrazione.
            return func(*args, **kwargs)

        return wrapper

    return decorator


@register_command()
def hello():
    return "Ciao! Questo è un comando registrato automaticamente."


@register_command("somma")
def add(a, b):
    return a + b


@register_command("moltiplica")
def multiply(a, b):
    return a * b


def execute_command(command_name, *args):
    """
    Cerca il comando nel registro e lo esegue.
    """
    if command_name not in COMMANDS:
        raise KeyError(f"Comando non trovato: {command_name}")

    return COMMANDS[command_name](*args)


if __name__ == "__main__":
    print("Comandi disponibili:", list(COMMANDS.keys()))

    print(execute_command("hello"))
    print(execute_command("somma", 2, 3))
    print(execute_command("moltiplica", 4, 5))

Spiegazione

Analizziamo il codice passo per passo, perché qui c’è un aspetto di metaprogrammazione molto interessante: il decoratore non si limita a modificare il comportamento della funzione, ma la registra in una struttura dati esterna.

1. Il dizionario COMMANDS

Il dizionario COMMANDS è il nostro registro centrale. La chiave è il nome del comando, mentre il valore è la funzione associata.

Per esempio:

  • "hello" punta alla funzione hello();
  • "somma" punta alla funzione add(a, b);
  • "moltiplica" punta alla funzione multiply(a, b).

2. Il decoratore parametrico register_command

La funzione register_command(name=None) è un decoratore con argomenti. Questo significa che prima riceve i parametri del decoratore e poi restituisce il vero decoratore.

La struttura è questa:

  • register_command(name=None) riceve il nome opzionale;
  • decorator(func) riceve la funzione da decorare;
  • wrapper(*args, **kwargs) esegue la funzione originale.

3. La registrazione automatica

Dentro decorator(func) calcoliamo il nome del comando:

  • se name è stato passato, usiamo quello;
  • altrimenti usiamo func.__name__.

Poi inseriamo la funzione nel dizionario:

COMMANDS[command_name] = func

Questo avviene nel momento in cui Python interpreta la definizione della funzione decorata, non quando la funzione viene chiamata. È qui che entra in gioco la metaprogrammazione: il decoratore agisce sulla struttura del programma durante il caricamento del modulo.

4. Il wrapper e functools.wraps

Il wrapper serve a mantenere la possibilità di aggiungere logiche extra. In questo esempio non fa molto, ma in progetti reali potresti usarlo per:

  • controllare permessi;
  • misurare tempi di esecuzione;
  • tracciare chiamate;
  • modificare input o output.

Abbiamo usato @wraps(func) per preservare metadati importanti della funzione originale, come:

  • __name__
  • __doc__
  • __module__

È una best practice fondamentale quando scrivi decoratori.

5. Esecuzione dinamica dei comandi

La funzione execute_command cerca il comando nel registro e lo esegue passando gli argomenti ricevuti. Questo approccio è molto utile quando non conosci in anticipo quale funzione dovrà essere eseguita.

In pratica, hai creato una piccola infrastruttura di dispatch dinamico senza ricorrere a catene di if/elif lunghissime.

Best practice

  • Usa functools.wraps sempre che il decoratore ritorni un wrapper. Evita di perdere nome, docstring e altri metadati utili.
  • Evita registri globali troppo complessi se il progetto cresce molto. Per applicazioni grandi, valuta classi dedicate o moduli separati per il registry.
  • Gestisci i conflitti di nome. Nel nostro esempio solleviamo un ValueError se un comando è già presente. È meglio fallire subito che sovrascrivere accidentalmente una funzione.
  • Separa registrazione ed esecuzione. Il decoratore dovrebbe occuparsi della registrazione; la logica di esecuzione dovrebbe stare altrove.
  • Documenta bene i comandi registrati. In sistemi plugin o CLI, una buona introspezione del registry aiuta molto il debug e la manutenzione.
  • Attenzione agli effetti collaterali all’import. Siccome la registrazione avviene quando il modulo viene importato, evita logiche pesanti o dipendenze circolari.

Riepilogo

In questo tutorial abbiamo visto un uso molto pratico dei decoratori: registrare automaticamente funzioni in un catalogo centrale. Questo pattern è una forma elegante di metaprogrammazione perché permette al codice di organizzare sé stesso senza configurazioni manuali ripetitive.

I punti chiave da ricordare sono:

  • un decoratore può eseguire logica al momento della definizione della funzione;
  • i decoratori parametrici sono ideali per configurare il comportamento;
  • un registry consente dispatch dinamico e architetture estensibili;
  • wraps è essenziale per mantenere pulita l’introspezione delle funzioni.

Questo pattern è semplice, ma scala bene in molti contesti reali: dai piccoli script ai sistemi modulari con plugin o comandi personalizzati.

Approfondisci con risorse ufficiali

SHARE