Ai văzut @something deasupra unei definiții de funcție în Flask, FastAPI, pytest, Django, SQLAlchemy și practic în orice bibliotecă Python scrisă după 2010. Simbolul @ arată ca și cum ar face magie. Nu face. Sunt două linii de cod deghizate. La sfârșitul acestei lecții vei putea citi orice decorator din orice cod și vei ști exact ce face.
Definiția în două linii
Un decorator e o funcție care ia o funcție și returnează o funcție. Atât.
from typing import Callable
def shout(fn: Callable[[str], str]) -> Callable[[str], str]:
def wrapped(name: str) -> str:
return fn(name).upper() + "!"
return wrapped
def greet(name: str) -> str:
return f"hello, {name}"
greet = shout(greet) # învelește-o
print(greet("narcis")) # HELLO, NARCIS!
Sintaxa @ e doar prescurtare pentru ultima atribuire. Cele două secvențe de mai jos sunt exact echivalente:
@shout
def greet(name: str) -> str:
return f"hello, {name}"
# vs.
def greet(name: str) -> str:
return f"hello, {name}"
greet = shout(greet)
Citește-l așa, greet = shout(greet), și decorators încetează să mai fie misterioși. Restul e variațiuni pe această temă.
Un decorator real: timing
Iată exemplul pe care îl vei scrie în cariera ta undeva între cinci și cincizeci de ori. Un decorator care măsoară cât durează o funcție.
from __future__ import annotations
import time
import functools
from typing import Callable, ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def timed(fn: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(fn)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
start = time.perf_counter()
try:
return fn(*args, **kwargs)
finally:
elapsed_ms = (time.perf_counter() - start) * 1000
print(f"{fn.__name__} took {elapsed_ms:.2f} ms")
return wrapper
@timed
def slow_add(a: int, b: int) -> int:
time.sleep(0.1)
return a + b
print(slow_add(2, 3))
# slow_add took 100.43 ms
# 5
Câteva lucruri merită semnalate:
*args, **kwargspermite wrapper-ului să accepte orice signature. Ăsta e pattern-ul standard pentru un decorator căruia nu-i pasă cum arată funcția dedesubt.ParamSpecșiTypeVar(Python 3.10+) păstrează signature-ul de tipuri al funcției originale. Înainte de 3.10 trebuia să alegi între tipuri exacte și un decorator generic. Acum poți avea pe amândouă.try/finallyasigură că timing-ul e printat chiar dacă funcția învelită ridică o excepție.@functools.wraps(fn)face cea mai importantă muncă din această secvență, și trebuie să discutăm despre el.
functools.wraps: linia pe care nu trebuie s-o omiți
Când învelești o funcție, wrapper-ul preia identitatea originalului. __name__, __doc__, __module__, __annotations__: toate aparțin acum wrapper-ului, nu funcției învelite. Asta strică uneltele de documentație, descoperirea testelor pytest, Sphinx, mesajele de traceback, orice face introspecție.
functools.wraps e un mic decorator care copiază acele atribute de la funcția învelită la wrapper.
import functools
from typing import Callable, ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def bare(fn: Callable[P, R]) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
return fn(*args, **kwargs)
return wrapper
def proper(fn: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(fn)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
return fn(*args, **kwargs)
return wrapper
@bare
def f1() -> None:
"""Original docstring."""
@proper
def f2() -> None:
"""Original docstring."""
print(f1.__name__, f1.__doc__) # wrapper None
print(f2.__name__, f2.__doc__) # f2 Original docstring.
Folosește functools.wraps. Întotdeauna. Fiecare decorator pe care îl scrii în cod de producție ar trebui să-l aibă. Costul e o linie. Costul de a-l uita e un coleg viitor care petrece două ore întrebându-se de ce toate stack trace-urile lui spun wrapper.
(Tangent: asistenții AI sunt extrem de buni la scris decorators, pentru că pattern-ul e atât de structurat: @functools.wraps, *args, **kwargs, interiorul def wrapper. Boilerplate-ul e identic în mii de exemple de antrenament. Ce ratează constant e thread-safety-ul. Dacă decoratorul tău cache-uiește rezultate într-un dict, sau mutează un counter, sau face append la o listă partajată, modelul va genera bucuros cod care intră în race condition. De fiecare dată când accepți cod de decorator scris de AI care atinge stare partajată, întreabă-te: ce se întâmplă dacă două thread-uri îl cheamă în același timp? De obicei răspunsul e „un RuntimeError peste șase luni în producție”.)
Stivuirea decoratorilor
Mai mulți decorators pe o singură funcție se compun de jos în sus. Decoratorul cel mai apropiat de funcție rulează primul.
@logged
@timed
def work(x: int) -> int:
return x * 2
Asta e echivalent cu:
work = logged(timed(work))
timed învelește work. Apoi logged învelește versiunea timed. Când chemi work(3), wrapper-ul cel mai exterior (logged) rulează primul, cheamă spre interior și se desface spre exterior. Ordinea contează: dacă logged ar decide să facă short-circuit pe input prost și să nu cheme spre interior, timed n-ar rula niciodată. Ordinea de stivuire schimbă comportamentul.
Decoratori cu argumente: dansul de ordinul trei
Aici se împiedică oamenii de decorators. Uneori vrei să parametrizezi decoratorul în sine:
@retry(times=3)
def fetch(url: str) -> bytes:
...
retry(times=3) trebuie să se evalueze la un decorator, adică la o funcție care ia o funcție și returnează o funcție. Deci retry e o funcție care returnează un decorator. Trei niveluri de def:
import functools
import time
from typing import Callable, ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def retry(times: int = 3, delay: float = 0.5) -> Callable[[Callable[P, R]], Callable[P, R]]:
def decorator(fn: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(fn)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
last_exc: BaseException | None = None
for attempt in range(1, times + 1):
try:
return fn(*args, **kwargs)
except Exception as exc:
last_exc = exc
if attempt < times:
time.sleep(delay)
assert last_exc is not None
raise last_exc
return wrapper
return decorator
@retry(times=3, delay=0.1)
def flaky() -> str:
...
Trei def-uri, fiecare cu o singură treabă:
retry(times, delay), cel mai exterior, captează configurația și returnează un decorator.decorator(fn), cel din mijloc, captează funcția decorată și returnează un wrapper.wrapper(*args, **kwargs), cel interior, captează fiecare apel și face munca propriu-zisă.
Dacă te uiți la el suficient de mult, încetează să mai pară ciudat. Dacă nu te uiți la el suficient de mult, vei continua să scrii @retry (fără paranteze) și să primești o eroare confuză. Parantezele sunt obligatorii când decoratorul ia argumente, chiar și un @retry() gol, pentru că sintaxa @ cere un decorator, nu o fabrică de decorators.
Decoratori în standard library
O jumătate de duzină de built-ins pe care îi vei folosi fără să te gândești:
import functools
class Order:
def __init__(self, total: float, vat_rate: float) -> None:
self._total = total
self._vat_rate = vat_rate
@property
def vat_amount(self) -> float:
return self._total * self._vat_rate
@staticmethod
def supported_currencies() -> list[str]:
return ["EUR", "USD", "GBP"]
@classmethod
def empty(cls) -> "Order":
return cls(total=0.0, vat_rate=0.0)
@functools.cache
def fib(n: int) -> int:
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
@functools.lru_cache(maxsize=128)
def expensive_lookup(key: str) -> dict[str, int]:
...
@propertyface ca o metodă să arate ca un atribut.order.vat_amountse citește mai bine decâtorder.vat_amount().@staticmethodși@classmethodschimbă cum se leagăself/cls. Utili, ocazional.@functools.cache(3.9+) memoizează o funcție cu un cache nelimitat. La fel calru_cache(maxsize=None), dar mai clar.@functools.lru_cache(maxsize=N)memoizează cu o politică LRU mărginită. Cheia de cache e tuplul de argumente; argumentele trebuie să fie hashable.
Cache-urile au footnote-ul de thread-safety pe care l-am menționat mai devreme. functools.lru_cache este thread-safe în CPython: operațiile dict-ului de dedesubt sunt protejate de GIL, dar un cache pe care-l scrii tu nu e, decât dacă pui un Lock în jurul read-modify-write-ului.
Decoratori în biblioteci populare
Pattern-uri pe care le vezi zilnic:
# Flask
@app.route("/orders/<int:order_id>", methods=["GET"])
def get_order(order_id: int) -> dict[str, object]:
...
# pytest
@pytest.fixture
def db_connection() -> Iterator[Connection]:
conn = connect()
yield conn
conn.close()
# tenacity
@retry(stop=stop_after_attempt(3), wait=wait_exponential())
def call_external_api() -> bytes:
...
# Click
@click.command()
@click.option("--country", default="IT")
def export(country: str) -> None:
...
N-ai nevoie să înțelegi măruntaiele bibliotecii ca să-i folosești. Ai nevoie să știi că urmăresc același pattern decorator(fn) -> fn pe care l-ai văzut mai sus. Când ceva merge prost (@app.route aruncă, fixture-ul de test nu e injectat), ajută să-ți amintești că nu e magie: doar mecanismul „funcție-care-returnează-funcție” pe care l-ai putea scrie singur la nevoie.
Decoratori pe bază de clase
Poți scrie un decorator și ca o clasă cu __call__:
import functools
from typing import Callable, ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
class CallCounter:
def __init__(self, fn: Callable[P, R]) -> None:
functools.update_wrapper(self, fn)
self.fn = fn
self.calls = 0
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
self.calls += 1
return self.fn(*args, **kwargs)
@CallCounter
def ping() -> str:
return "pong"
ping(); ping(); ping()
print(ping.calls) # 3
Util când decoratorul are nevoie de stare persistentă și vrei să o accesezi din afară. functools.update_wrapper e forma de funcție a lui @functools.wraps: aceeași treabă.
Când să NU scrii un decorator
Un decorator e o unealtă grozavă când:
- Comportamentul învelește o funcție (înainte/după/în jur).
- Vrei aceeași învelire în mai multe locuri.
- Învelirea e ortogonală cu ce face funcția (timing, caching, retries, verificări de auth).
E unealta greșită când:
- Comportamentul aparține în interiorul corpului funcției: scrie un helper.
- Ai nevoie de semantică acquire/release în jurul unui bloc de cod, nu al unei funcții: folosește un context manager (lecția următoare).
- Înveli o singură funcție, vreodată: scrie logica direct, inline.
Mirosul de urmărit: un decorator cu un obiect de configurare uriaș și un if context.something: do_a() else: do_b() condițional în interiorul wrapper-ului. Aia e o funcție mascată ca decorator. Refactorizeaz-o.
Asta a fost despre decorators. Lecția următoare: context managers, blocul with, trei moduri de a-ți scrie propriul context manager și pattern-urile care fac try/finally redundant.
Citations (consultat 2026-05-01):
- PEP 318, “Decorators for Functions and Methods” - https://peps.python.org/pep-0318/
- PEP 612, “Parameter Specification Variables” (
ParamSpec) - https://peps.python.org/pep-0612/ functoolsmodule documentation - https://docs.python.org/3/library/functools.html- Python Language Reference, “Function definitions” - https://docs.python.org/3/reference/compound_stmts.html#function-definitions