Parliamo

project-cherry.dev

Avanzato18 min

Decoratori e Generatori

Costruisci un toolkit professionale: decoratori per misurare performance e tracciare operazioni, generatori per elaborare grandi volumi di dati senza esaurire la memoria.

Decoratori e Generatori

Immagina il toolkit della consulente fiscale Sara Conti. Ogni trimestre elabora migliaia di fatture, deve misurare quanto tempo impiegano i calcoli pesanti, tenere un log di ogni operazione per le verifiche dell'Agenzia delle Entrate e processare file CSV da 100.000 righe senza far esplodere la memoria del portatile. Decoratori e generatori risolvono esattamente questi problemi.

Il concetto di decoratore

Un decoratore è una funzione che avvolge un'altra funzione, aggiungendo comportamento prima, dopo o intorno all'esecuzione originale. Pensalo come un involucro trasparente: la funzione interna non cambia, ma acquista superpoteri.

python
def decoratore(funzione_originale):
    def wrapper(*args, **kwargs):
        # --- Codice aggiuntivo PRIMA ---
        print(f"Chiamo {funzione_originale.__name__}")
        risultato = funzione_originale(*args, **kwargs)
        # --- Codice aggiuntivo DOPO ---
        print(f"Completato {funzione_originale.__name__}")
        return risultato
    return wrapper

La sintassi @decoratore sopra una funzione è zucchero sintattico per funzione = decoratore(funzione).

Decoratore cronometro

Sara vuole sapere quanto impiegano i calcoli IVA su grandi dataset. Un decoratore cronometro risolve il problema senza toccare il codice dei calcoli.

python
import time
import functools

def cronometro(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        inizio = time.time()
        risultato = func(*args, **kwargs)
        durata = time.time() - inizio
        print(f"[PERF] {func.__name__} completata in {durata:.4f}s")
        return risultato
    return wrapper

@cronometro
def calcola_iva_trimestre(fatture):
    """Calcola il totale IVA per il trimestre."""
    totale_iva = sum(f["importo"] * f["aliquota_iva"] / 100 for f in fatture)
    return totale_iva

# Simuliamo 10.000 fatture
fatture = [{"importo": 1000, "aliquota_iva": 22} for _ in range(10_000)]
iva = calcola_iva_trimestre(fatture)
# [PERF] calcola_iva_trimestre completata in 0.0012s
print(f"IVA totale: {iva:.2f} EUR")  # IVA totale: 2200000.00 EUR

Il @functools.wraps(func) preserva il nome e la docstring della funzione originale — un dettaglio importante per il debugging.

Decoratore di logging e audit

Per le verifiche fiscali serve un registro di ogni operazione eseguita sulle fatture: chi ha fatto cosa e quando.

python
import datetime

def audit_log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        print(f"[AUDIT] {timestamp} - Esecuzione: {func.__name__}")
        risultato = func(*args, **kwargs)
        print(f"[AUDIT] {timestamp} - Completata: {func.__name__}")
        return risultato
    return wrapper

@audit_log
@cronometro
def registra_pagamento(numero_fattura, importo):
    """Registra il pagamento di una fattura."""
    print(f"Pagamento registrato: fattura {numero_fattura}, {importo:.2f} EUR")
    return True

registra_pagamento("2024-157", 2500.00)
# [AUDIT] 2024-11-15 10:30:00 - Esecuzione: registra_pagamento
# [PERF] registra_pagamento completata in 0.0001s
# Pagamento registrato: fattura 2024-157, 2500.00 EUR
# [AUDIT] 2024-11-15 10:30:00 - Completata: registra_pagamento

I decoratori si impilano: @audit_log avvolge @cronometro, che avvolge la funzione. L'ordine conta.

Decoratore con parametri

A volte serve configurare il decoratore. Sara vuole un livello di log variabile: "info" per le operazioni normali, "warning" per quelle critiche.

python
def log_con_livello(livello="info"):
    def decoratore(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f"[{livello.upper()}] Inizio {func.__name__}")
            risultato = func(*args, **kwargs)
            print(f"[{livello.upper()}] Fine {func.__name__}")
            return risultato
        return wrapper
    return decoratore

@log_con_livello(livello="warning")
def elimina_fattura(numero):
    """Elimina una fattura — operazione critica."""
    print(f"Fattura {numero} eliminata")

elimina_fattura("2024-003")
# [WARNING] Inizio elimina_fattura
# Fattura 2024-003 eliminata
# [WARNING] Fine elimina_fattura

Nota la struttura a tre livelli: funzione esterna (riceve i parametri), decoratore (riceve la funzione), wrapper (esegue la logica).

Generatore per leggere CSV enormi

Sara riceve un CSV con 100.000 righe di transazioni bancarie. Caricarlo tutto in memoria con una lista rischierebbe di saturare la RAM. Un generatore legge una riga alla volta.

python
def leggi_fatture_csv(percorso):
    """Genera fatture una alla volta da un CSV enorme."""
    import csv
    with open(percorso, "r", encoding="utf-8") as f:
        lettore = csv.DictReader(f)
        for riga in lettore:
            yield {
                "numero": riga["numero"],
                "importo": float(riga["importo"]),
                "stato": riga["stato"],
            }

# Elabora 100k righe senza caricarle tutte in memoria
totale = 0
conteggio = 0
for fattura in leggi_fatture_csv("transazioni_2024.csv"):
    if fattura["stato"] == "pagata":
        totale += fattura["importo"]
        conteggio += 1

print(f"Fatture pagate: {conteggio}, totale: {totale:.2f} EUR")

Il yield restituisce un valore e mette in pausa la funzione fino alla richiesta successiva. La memoria usata è costante, indipendentemente dalla dimensione del file.

Generator expression vs list comprehension

python
# List comprehension: crea TUTTA la lista in memoria
importi_lista = [f["importo"] for f in fatture]  # occupa memoria per 100k float

# Generator expression: calcola un valore alla volta
importi_gen = (f["importo"] for f in fatture)  # quasi zero memoria

# Entrambi funzionano con sum(), max(), min()
totale = sum(f["importo"] for f in fatture)

Usa le parentesi tonde invece delle quadre: la differenza è enorme con grandi dataset.

Combinare decoratori e generatori

Il vero potere emerge combinandoli: un decoratore cronometro su una pipeline che usa generatori per processare dati in streaming.

python
@cronometro
def analisi_trimestrale(percorso_csv):
    """Pipeline completa: leggi, filtra, calcola."""
    fatture = leggi_fatture_csv(percorso_csv)
    pagate = (f for f in fatture if f["stato"] == "pagata")
    totale = sum(f["importo"] for f in pagate)
    return totale

Il generatore scorre il file una sola volta, il decoratore misura il tempo. Nessuna lista intermedia viene creata in memoria.

Da ricordare

  • Un decoratore avvolge una funzione aggiungendo comportamento senza modificarla
  • Usa @functools.wraps per preservare nome e docstring della funzione originale
  • I decoratori con parametri richiedono tre livelli di funzioni annidate
  • I generatori con yield producono valori uno alla volta, ideali per file enormi
  • Le generator expression (x for x in ...) sono l'alternativa lazy alle list comprehension
  • Combinare decoratori e generatori crea pipeline efficienti e tracciabili