Module lessons (4/4)
Decorators: functions that modify functions
A decorator is a function that takes another function and returns a "decorated" version of it. It is a powerful way to add behavior (logging, timing, caching, retry, validation) without touching the body of the original function.
Functions as values
In Python, functions are objects: you can pass them, return them, assign them.
def saluta():
print("ciao")
f = saluta # NIENTE parentesi: assegno la funzione
f() # 'ciao'A minimal decorator
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@decorator syntax
The assignment somma = log(somma) is written more elegantly with @:
@log
def somma(a, b):
return a + bIt is exactly the same thing. @log above the definition is equivalent to:
"as soon as this function is defined, replace it with log(function)".
*args, **kwargs: accept any signature
def wrapper(*args, **kwargs):
return func(*args, **kwargs)*args collects positional arguments, **kwargs keyword arguments. They let
you write wrappers that work with any function.
functools.wraps: preserve metadata
Without wraps, the decorated function loses __name__, __doc__, hints:
from functools import wraps
def log(func):
@wraps(func)
def wrapper(*args, **kwargs):
...
return func(*args, **kwargs)
return wrapperExample: @timeit
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()Decorators from the stdlib
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— memoizes the results of a pure function.@property— exposes a method as an attribute (seen in the OOP module).@staticmethod,@classmethod— change howselfis passed.
Decorators with arguments
Decorators can also accept arguments. To implement this, you must write three levels of nested functions: the outermost function receives the decorator's arguments, the middle function receives the decorated function, and the innermost function (wrapper) receives the actual arguments passed to the decorated function during calls.
Try it yourself
Write a decorator `double_result` that calls the decorated function and returns its result * 2. Apply it to a function `f(x)` that returns x + 1. Evaluate `f(10)`.
Show hint
The wrapper calls func and returns the result multiplied by 2.
Solution available after 3 attempts
Review exercise
Use functools.lru_cache to speed up a recursive fib(n) function. Compute fib(30) and assign to `r`. Evaluate `r`.
Show hint
@lru_cache(maxsize=None) above the def.
Solution available after 3 attempts
Additional challenge
Define a decorator `double_result` that doubles (multiplies by 2) the return value of the decorated function. Apply the decorator to the function `def sum_nums(a, b): return a + b`. Finally, evaluate `sum_nums(3, 4)`.
Show hint
The wrapper calls func(*args, **kwargs) and multiplies the result by 2.
Solution available after 3 attempts