Learning Platform
Глоссарий Troubleshooting
Урок 07.01 · 22 мин
Продвинутый
DecoratorsClosurePyCellObjectcell_contentsHigher-order functionsAspect-orientedTimingRecipe

Function decorators: что внутри @decorator

Декоратор — это функция, которая принимает функцию и возвращает функцию (обычно — обёрнутую). На уровне синтаксиса @decorator поверх def f — sugar для одного присваивания: f = decorator(f). Никакой магии: имя f после декоратора связано не с оригинальной функцией, а с тем, что вернул decorator(f) — обычно это новая функция-wrapper, замыкающая оригинальную через closure.

Если урок 04 в M03 уже познакомил вас с closure (PyCellObject + LOAD_DEREF), то декоратор — это прикладной случай того же механизма. В этом уроке покажем:

  1. Sugar @dec ≡ f = dec(f) — буквальное переписывание.
  2. Closure-based wrapper revisitwrapper.__closure__[0].cell_contents empirically возвращает оригинальный fn.
  3. Decorator-with-args — three nested def (factory → real_decorator → wrapper).
  4. Recipe end-to-end: @timing декоратор для measurement execution time — production-ready observability primitive.
  5. 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). Оба вызова — на уровне исходника одинаковы; @-форма — просто короче и видно сверху функции.

TIP

Несколько декораторов — применяются снизу вверх: @a @b def ff = 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).

WARNING

Внимание: 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.

WARNING

Pitfall: забыли вызывающие ()@decorator_factory (без аргументов) применит decorator_factory напрямую к функции, передав её как param. Wrapper получит param=greet (саму функцию), fn будет undefined. Силлогизм: декоратор-без-аргументов и декоратор-с-аргументами — разные signatures; compiler не предупредит.


Diagram: closure → wrapper → call site

caller scope
wrapper (function obj)
cell (PyCellObject)
original add (function obj)

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

Что мы получили:

  1. Не меняли slow_add — её код не знает, что её декорировали.
  2. Cross-cutting concern (timing) отделён от business logic (сложение). Это aspect-oriented programming.
  3. Composable — можно навесить @timing на любую функцию: ETL pipeline step, query, aggregation. Один декоратор — N use cases.
TIP

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


В 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 = 10Empirically: wrapper.__closure__[0].cell_contents IS оригинальная fn
LOAD_DEREF в bytecodeТот же LOAD_DEREF в wrapper’е

Декоратор — не новый mechanism, а компактный pattern, использующий closure для cross-cutting concerns.


Pitfall checklist

#TrapFix
1@dec без () для параметризованного декоратора@retry(3), не @retry
2Несколько декораторов — порядок применения@a @b def ff = a(b(f)) (снизу вверх)
3wrapper без @functools.wraps теряет __name__/__doc__/etc.См. урок 02
4Cells — implementation detail, не stable indexinspect.getclosurevars для introspection
5Class-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.


Ключевые выводы

  1. @decf = dec(f) — синтаксический сахар для одного присваивания; compiler буквально переписывает.
  2. Closure-based wrapper — наиболее частый pattern; wrapper.__closure__[0].cell_contents ессlib оригинальная fn (revisit M03 урок 04 — тот же PyCellObject + LOAD_DEREF).
  3. LOAD_DEREF opcode — bytecode primitive, читающий из cell; не новый mechanism для декораторов.
  4. Decorator-with-args — three nested def (factory → real_decorator → wrapper); каждый уровень — closure.
  5. @timing recipe — production-ready aspect-oriented примитив для observability; отделяет cross-cutting concern (timing) от business logic.
  6. Why decorators — cross-cutting concerns (auth, logging, retry, caching) декларативно навешиваются через @, без дублирования boilerplate.

В уроке 02 — лечим __name__/__doc__ через @functools.wraps + параметризованный @retry end-to-end.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что эквивалентно `@my_decorator def f(): ...`?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5