Урок по треку Python 7 мин

Декораторы в Python: от простого до параметризованного

Функции как объекты, декораторы с параметрами, functools.wraps, lru_cache и декораторы классов.

Декоратор это функция, которая принимает функцию и возвращает другую функцию (обычно обёртку). Задача «напишите декоратор» есть почти на каждом Python-собесе, от junior до senior.

Синтаксис это сахар

@timer
def handler(): ...

ровно то же самое, что

def handler(): ...
handler = timer(handler)

Декоратор срабатывает один раз при определении функции (обычно при импорте модуля), а обёртка срабатывает при каждом вызове.

Базовый шаблон

import functools
import time

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            return func(*args, **kwargs)
        finally:
            print(f'{func.__name__}: {time.perf_counter() - start:.3f} c')
    return wrapper

Три обязательных детали:

  1. *args, **kwargs, чтобы обёртка принимала любые аргументы.
  2. return func(...), иначе декорированная функция начнёт возвращать None.
  3. functools.wraps, чтобы сохранить __name__, __doc__, __qualname__, __module__ и добавить __wrapped__. Без него логи, отладчик, документация и фреймворки (например, роутинг по имени функции) видят wrapper.

Декоратор с параметрами

@retry(times=3) сначала вызывает retry(times=3), и уже результат применяется к функции. Получается три уровня вложенности:

def retry(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(times):
                try:
                    return func(*args, **kwargs)
                except ConnectionError:
                    if attempt == times - 1:
                        raise
        return wrapper
    return decorator

Порядок применения

@a
@b
def f(): ...

это f = a(b(f)): ближайший к функции декоратор применяется первым, а при вызове первой срабатывает внешняя обёртка a.

Состояние в декораторе

Обёртка может хранить состояние в замыкании или в атрибуте: wrapper.calls = 0. Это состояние общее для всех вызовов функции, поэтому в многопоточном коде его защищают блокировкой.

Декораторы из стандартной библиотеки

  • functools.cache и functools.lru_cache(maxsize=128): мемоизация, аргументы должны быть хешируемыми. У lru_cache есть cache_info() и cache_clear().
  • @property, @staticmethod, @classmethod: декораторы методов.
  • @dataclass, @functools.total_ordering: декораторы классов, они дописывают методы.
  • contextlib.contextmanager: делает контекстный менеджер из генератора.

Декоратор-класс и декоратор класса

Декоратором может быть любой вызываемый объект, например класс с __call__. А «декоратор класса» принимает класс и возвращает класс: так работает @dataclass.

Частые ошибки

  • Забыли return результата в обёртке.
  • Нет functools.wraps.
  • Перепутали уровни у параметризованного декоратора: @retry без скобок передаст функцию в times.
  • lru_cache на методе экземпляра: кеш держит ссылки на self и не даёт объектам умереть.
  • Тяжёлая работа в теле декоратора выполняется при импорте.

Что спросят на собесе

  • Напишите декоратор, который замеряет время или логирует вызовы.
  • Зачем functools.wraps.
  • Как сделать декоратор с параметрами и в каком порядке применяются несколько декораторов.
  • Когда выполняется код декоратора и когда код обёртки.
  • Как работает lru_cache и какие у него ограничения.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.