Vai al contenuto
eLearner.app
Modulo 9 · Lezione 4 di 436/36 nel corso~14 min
Lezioni del modulo (4/4)

Decoratori: funzioni che modificano funzioni

Un decoratore è una funzione che riceve un'altra funzione e ne ritorna una versione "decorata". È un modo potente per aggiungere comportamento (logging, timing, cache, retry, validazione) senza toccare il corpo della funzione originale.

Funzioni come valori

In Python le funzioni sono oggetti: le puoi passare, ritornare, assegnare.

Python
def saluta():
    print("ciao")

f = saluta   # NIENTE parentesi: assegno la funzione
f()          # 'ciao'

Un decoratore minimale

Python
def log(func):
    def wrapper(*args, **kwargs):
        print(f"chiamo {func.__name__}")
        risultato = func(*args, **kwargs)
        print(f"ho finito {func.__name__}")
        return risultato
    return wrapper

def somma(a, b):
    return a + b

somma = log(somma)   # ora somma è "wrapper"
somma(2, 3)
# chiamo somma
# ho finito somma
# 5

Sintassi @decorator

L'assegnazione somma = log(somma) si scrive più elegantemente con @:

Python
@log
def somma(a, b):
    return a + b

È esattamente la stessa cosa. @log sopra la definizione equivale a: "appena definita questa funzione, sostituiscila con log(funzione)".

*args, **kwargs: accetta qualsiasi firma

Python
def wrapper(*args, **kwargs):
    return func(*args, **kwargs)

*args raccoglie i posizionali, **kwargs i nominati. Servono per scrivere wrapper che funzionano con qualunque funzione.

functools.wraps: preserva i metadati

Senza wraps, la funzione decorata perde __name__, __doc__, hint:

Python
from functools import wraps

def log(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        ...
        return func(*args, **kwargs)
    return wrapper

Esempio: @timeit

Python
from functools import wraps
import time

def timeit(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        t0 = time.perf_counter()
        risultato = func(*args, **kwargs)
        dt = time.perf_counter() - t0
        print(f"{func.__name__} ha impiegato {dt*1000:.2f} ms")
        return risultato
    return wrapper

@timeit
def somma_grande():
    return sum(range(10**6))

somma_grande()

Decoratori dalla stdlib

Python
from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n < 2: return n
    return fib(n - 1) + fib(n - 2)

fib(100)  # istantaneo grazie alla cache
  • @lru_cache — memorizza i risultati di una funzione pura.
  • @property — espone un metodo come attributo (visto nel modulo OOP).
  • @staticmethod, @classmethod — modificano come viene passato self.

Decoratori con argomenti

I decoratori possono anche accettare argomenti. Per farlo, occorre definire tre livelli di funzioni annidate: il livello esterno riceve gli argomenti del decoratore, il secondo livello riceve la funzione da decorare, ed il terzo livello (wrapper) riceve i parametri reali passati alla funzione decorata durante la chiamata.

Prova tu

Esercizio#python.m9.l4.e1
Tentativi: 0Caricamento…

Scrivi un decoratore `double_result` che chiama la funzione decorata e ritorna il suo risultato * 2. Applicalo a una funzione `f(x)` che ritorna x + 1. Valuta `f(10)`.

Caricamento editor…
Mostra suggerimento

Il wrapper chiama func e ritorna il risultato moltiplicato per 2.

Soluzione disponibile dopo 3 tentativi

Esercizio di ripasso

Esercizio#python.m9.l4.e2
Tentativi: 0Caricamento…

Usa functools.lru_cache per accelerare una funzione fib(n) ricorsiva. Calcola fib(30) e assegna a `r`. Valuta `r`.

Caricamento editor…
Mostra suggerimento

@lru_cache(maxsize=None) sopra la def.

Soluzione disponibile dopo 3 tentativi

Sfida aggiuntiva

Esercizio#python.m9.l4.e3
Tentativi: 0Caricamento…

Definisci un decoratore `double_result` che raddoppi (moltiplicando per 2) il valore restituito dalla funzione decorata. Applica il decoratore alla funzione `def sum_nums(a, b): return a + b`. Valuta infine `sum_nums(3, 4)`.

Caricamento editor…
Mostra suggerimento

Il wrapper chiama func(*args, **kwargs) e moltiplica il risultato per 2.

Soluzione disponibile dopo 3 tentativi