Skip to main content
eLearner.app
Module 9 · Lesson 4 of 436/36 in the course~14 min
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.

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

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

A minimal decorator

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

@decorator syntax

The assignment somma = log(somma) is written more elegantly with @:

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

It 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

Python
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:

Python
from functools import wraps

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

Example: @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()

Decorators from the 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 — memoizes the results of a pure function.
  • @property — exposes a method as an attribute (seen in the OOP module).
  • @staticmethod, @classmethod — change how self is 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

Exercise#python.m9.l4.e1
Attempts: 0Loading…

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)`.

Loading editor…
Show hint

The wrapper calls func and returns the result multiplied by 2.

Solution available after 3 attempts

Review exercise

Exercise#python.m9.l4.e2
Attempts: 0Loading…

Use functools.lru_cache to speed up a recursive fib(n) function. Compute fib(30) and assign to `r`. Evaluate `r`.

Loading editor…
Show hint

@lru_cache(maxsize=None) above the def.

Solution available after 3 attempts

Additional challenge

Exercise#python.m9.l4.e3
Attempts: 0Loading…

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)`.

Loading editor…
Show hint

The wrapper calls func(*args, **kwargs) and multiplies the result by 2.

Solution available after 3 attempts