Function decorators: что внутри @decorator
Декоратор — это функция, которая принимает функцию и возвращает функцию (обычно — обёрнутую). На уровне синтаксиса @decorator поверх def f — sugar для одного присваивания: f = decorator(f). Никакой магии: имя f после декоратора связано не с оригинальной функцией, а с тем, что вернул decorator(f) — обычно это новая функция-wrapper, замыкающая оригинальную через closure.
Если урок 04 в M03 уже познакомил вас с closure (PyCellObject + LOAD_DEREF), то декоратор — это прикладной случай того же механизма. В этом уроке покажем:
- Sugar
@dec ≡ f = dec(f)— буквальное переписывание. - Closure-based wrapper revisit —
wrapper.__closure__[0].cell_contentsempirically возвращает оригинальныйfn. - Decorator-with-args — three nested
def(factory → real_decorator → wrapper). - Recipe end-to-end:
@timingдекоратор для measurement execution time — production-ready observability primitive. - Why decorators? — aspect-oriented programming: cross-cutting concerns (logging, timing, retry, auth) отделены от business logic.
Это pragmatic-DEEP урок: 50% — рецепты production-применения, 50% — internals (closure cells revisit). Внутренности нужны не для того, чтобы переписывать functools руками, а чтобы понимать, почему @dec работает и как debug’ить, когда что-то идёт не так.
Sugar: @dec — это просто f = dec(f)
Декоратор @d поверх def f(...) — буквальный синтаксический эквивалент строки f = d(f). Compiler делает одну замену: после исполнения тела def f сразу вызывает d(f) и связывает имя f с результатом.
def my_decorator(fn):
print(f'decorating {fn.__name__}')
return fn # вернём fn без изменений — это валидный тривиальный декоратор
@my_decorator
def hello():
return 'hello'
# Эквивалентно:
def hello():
return 'hello'
hello = my_decorator(hello)
Compiler сначала исполнит def hello(...) (создаст function object), потом сразу hello = my_decorator(hello). Оба вызова — на уровне исходника одинаковы; @-форма — просто короче и видно сверху функции.
Несколько декораторов — применяются снизу вверх: @a @b def f ≡ f = a(b(f)). Внутренний декоратор отрабатывает первым. Это часто противоинтуитивно для новичков.
Closure-based wrapper: revisit M03 урок 04
Самый частый паттерн — wrapper, замыкающий оригинальную функцию. Это closure: nested def внутри def, и внутренняя функция ссылается на параметр внешней (fn).
def loud(fn):
def wrapper(*args, **kwargs):
print(f'calling {fn.__name__}')
result = fn(*args, **kwargs)
print(f'{fn.__name__} returned {result!r}')
return result
return wrapper
@loud
def add(a, b):
return a + b
add(2, 3)
# calling add
# add returned 5
# 5
Что происходит на уровне CPython? Compiler видит, что wrapper ссылается на fn из enclosing scope, помечает fn как cell variable в code-object’е loud, и для wrapper создаёт closure — кортеж из cell-объектов. Каждая cell — это PyCellObject (см. Objects/cellobject.c) с одним полем ob_ref, указывающим на захваченный объект.
Empirically — открываем __closure__:
def loud(fn):
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
@loud
def add(a, b):
return a + b
print(add.__closure__) # (<cell at 0x...: function object at 0x...>,)
print(add.__closure__[0]) # <cell at 0x...: function object at 0x...>
print(add.__closure__[0].cell_contents) # <function add at 0x...> ← оригинальная!
print(add.__closure__[0].cell_contents.__name__) # 'add'
add.__closure__[0].cell_contents есть оригинальная add (до декорирования). После @loud имя add связано с wrapper’ом, но оригинал жив внутри cell — без него wrapper не сможет вызвать функцию. Это в точности тот же механизм, что в M03 урок 04 (outer().inner захватывает x из enclosing scope).
Внимание: cell_contents для нескольких captured переменных — __closure__ это tuple, индексы соответствуют порядку, в котором compiler решил их хранить (lexical, не stable между Python versions). Полагаться на конкретный индекс можно для debug, не для production logic — используйте inspect.getclosurevars(wrapper) для introspection.
import inspect
print(inspect.getclosurevars(add).nonlocals)
# {'fn': <function add at 0x...>}
Это читаемая альтернатива — словарь {имя_переменной: захваченное_значение}.
Bytecode level: LOAD_DEREF / STORE_DEREF
Когда wrapper обращается к fn, compiler не использует LOAD_FAST (как для локальных) и не LOAD_GLOBAL — он использует LOAD_DEREF, который читает из cell:
import dis
def loud(fn):
def wrapper():
return fn() # ← LOAD_DEREF 'fn'
return wrapper
dis.dis(loud)
# Вывод (упрощено):
# loud:
# ...
# MAKE_CELL 'fn' ← создаём cell для fn
# ...
# LOAD_CLOSURE 'fn' ← кладём cell в стек
# BUILD_TUPLE 1
# LOAD_CONST <code wrapper>
# MAKE_FUNCTION (closure) ← связываем code object с closure tuple
# ...
# wrapper:
# ...
# LOAD_DEREF 'fn' ← читаем из cell (НЕ LOAD_FAST!)
# CALL 0
# ...
Cite: Python/compile.c — функция compile_make_closure собирает closure tuple; Python/ceval.c — обработчик LOAD_DEREF opcode читает cell->ob_ref (поле PyCellObject).
Это тот же mechanism, что в M03 урок 04 — никакого нового primitive. Декоратор — закономерное прикладное использование closure в Python.
Decorator with args: three nested def
Если декоратор должен принимать параметры (@retry(max_attempts=3), @cache(maxsize=128)), синтаксически это уже не одно вызывание, а двухступенчатое: сначала retry(max_attempts=3) возвращает декоратор, потом он применяется к функции. В коде это three nested def:
def decorator_factory(param): # 1-й уровень: принимает параметры
def real_decorator(fn): # 2-й уровень: принимает функцию
def wrapper(*args, **kwargs): # 3-й уровень: wrapper для вызова
print(f'param={param}')
return fn(*args, **kwargs)
return wrapper
return real_decorator
@decorator_factory(param='hello')
def greet():
return 'hi'
greet()
# param=hello
# 'hi'
Эквивалент без @-sugar:
def greet():
return 'hi'
greet = decorator_factory(param='hello')(greet) # двойной вызов!
Семантика: decorator_factory(param='hello') возвращает real_decorator; затем real_decorator(greet) возвращает wrapper; имя greet теперь связано с wrapper’ом. Каждый уровень def — closure: real_decorator замыкает param, wrapper замыкает и param, и fn.
Pitfall: забыли вызывающие () — @decorator_factory (без аргументов) применит decorator_factory напрямую к функции, передав её как param. Wrapper получит param=greet (саму функцию), fn будет undefined. Силлогизм: декоратор-без-аргументов и декоратор-с-аргументами — разные signatures; compiler не предупредит.
Diagram: closure → wrapper → call site
Caller вызывает add(2, 3) — но add это wrapper. wrapper через LOAD_DEREF читает из cell, получает fn (оригинальную add), вызывает её с (2, 3). Результат 5 идёт обратно через wrapper → caller’у.
Recipe end-to-end: @timing decorator
Production-ready observability primitive: декоратор, измеряющий execution time каждого вызова. Это bread and butter для data-engineering pipelines — где без timing невозможно отслеживать regressions.
import time
def timing(fn):
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = fn(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f'{fn.__name__} took {elapsed:.6f}s')
return result
return wrapper
@timing
def slow_add(a, b):
time.sleep(0.001) # симулируем работу
return a + b
print(slow_add(2, 3))
# slow_add took 0.001234s
# 5
Что мы получили:
- Не меняли
slow_add— её код не знает, что её декорировали. - Cross-cutting concern (timing) отделён от business logic (сложение). Это aspect-oriented programming.
- Composable — можно навесить
@timingна любую функцию: ETL pipeline step, query, aggregation. Один декоратор — N use cases.
Production-уточнение: print подходит для развед-боя. Для production используйте logging (logger.debug или logger.info с structured log fields). Для production observability — Prometheus histogram’ы, OpenTelemetry spans. @timing decorator — pattern; конкретная реализация adapt’ится под infrastructure.
Pitfall: в этом wrapper нет @functools.wraps — slow_add.__name__ после декорирования вернёт 'wrapper', не 'slow_add'. Это ломает introspection (e.g., pytest report, Sphinx autodoc, traceback). В уроке 02 покажем, как @functools.wraps это лечит.
Why decorators? — aspect-oriented programming
Cross-cutting concerns — это аспекты, которые «пересекают» multiple функций / классов: logging, timing, auth check, retry, caching, validation. Без декораторов их пришлось бы дублировать в каждой функции:
# Без декораторов (anti-pattern):
def process_user(user_id):
start = time.perf_counter()
if not is_authenticated():
raise PermissionError('not authenticated')
try:
# ... business logic ...
return user
finally:
elapsed = time.perf_counter() - start
log.info(f'process_user took {elapsed}s')
def process_order(order_id):
start = time.perf_counter()
if not is_authenticated(): # ← duplicated boilerplate!
raise PermissionError('not authenticated')
try:
# ... business logic ...
return order
finally:
elapsed = time.perf_counter() - start
log.info(f'process_order took {elapsed}s')
С декораторами — отделено:
@timing
@require_auth
def process_user(user_id):
return user
@timing
@require_auth
def process_order(order_id):
return order
Бизнес-логика прозрачна; cross-cutting concerns декларативно навешены. Это и есть aspect-oriented programming в Python — не отдельный язык (как AspectJ для Java), а pattern, идиоматично реализуемый через @-sugar и closure.
Production data-engineering examples:
@retry(max_attempts=3)— для flaky DB / API calls.@cache(functools.lru_cache) — мемоизация expensive pure functions.@profile— line-by-line profiling в dev (pyspy / line_profiler).@event_handler('user.created')— registry pattern для event-driven systems.@app.route('/users')(Flask) /@app.get('/users')(FastAPI) — registry routes.
Каждый из этих декораторов под капотом — closure-based wrapper или class-based (урок 03).
Cross-link M03 урок 04: closure → decorator
В M03 урок 04 мы видели closure через lru_cache-pattern и через partial. Декоратор — это тот же механизм, применённый к одной задаче: обернуть функцию. Если поняли M03 урок 04 (PyCellObject + LOAD_DEREF), декоратор — natural extension.
Что новое в M06 урок 01 vs M03 урок 04:
| M03 урок 04 (closure) | M06 урок 01 (decorator) |
|---|---|
| Closure как примитив | Decorator как прикладной use case closure |
partial(func, x) хранит args в cells | @decorator оборачивает функцию через wrapper |
Empirically: f.__closure__[0].cell_contents = 10 | Empirically: wrapper.__closure__[0].cell_contents IS оригинальная fn |
LOAD_DEREF в bytecode | Тот же LOAD_DEREF в wrapper’е |
Декоратор — не новый mechanism, а компактный pattern, использующий closure для cross-cutting concerns.
Pitfall checklist
| # | Trap | Fix |
|---|---|---|
| 1 | @dec без () для параметризованного декоратора | @retry(3), не @retry |
| 2 | Несколько декораторов — порядок применения | @a @b def f ≡ f = a(b(f)) (снизу вверх) |
| 3 | wrapper без @functools.wraps теряет __name__/__doc__/etc. | См. урок 02 |
| 4 | Cells — implementation detail, не stable index | inspect.getclosurevars для introspection |
| 5 | Class-based decorator теряет __name__ функции | См. урок 03 + functools.update_wrapper(self, fn) |
Cross-course context
Cross-course → Spark: 06/01 builtin-vs-udfs — Spark
@udfдекоратор регистрирует Python-функцию как Spark SQL UDF; та же closure-pattern, что и в обычном@dec: фабрика возвращает wrapper, который оборачивает оригинал и добавляет cross-cutting concern (в Spark — сериализация args, передача driver→executor через cloudpickle, схема возврата). Decorator-as-pattern: один и тот же синтаксисf = dec(f), но wrapper делает разные вещи в зависимости от runtime — local@timingпротив@udfдля distributed Spark execution.
Ключевые выводы
@dec≡f = dec(f)— синтаксический сахар для одного присваивания; compiler буквально переписывает.- Closure-based wrapper — наиболее частый pattern;
wrapper.__closure__[0].cell_contentsессlib оригинальнаяfn(revisit M03 урок 04 — тот жеPyCellObject+LOAD_DEREF). LOAD_DEREFopcode — bytecode primitive, читающий из cell; не новый mechanism для декораторов.- Decorator-with-args — three nested
def(factory → real_decorator → wrapper); каждый уровень — closure. @timingrecipe — production-ready aspect-oriented примитив для observability; отделяет cross-cutting concern (timing) от business logic.- Why decorators — cross-cutting concerns (auth, logging, retry, caching) декларативно навешиваются через
@, без дублирования boilerplate.
В уроке 02 — лечим __name__/__doc__ через @functools.wraps + параметризованный @retry end-to-end.