Декоратор — функция, которая принимает функцию и возвращает новую, с добавленным поведением. Строка @timer над def slow() не делает ничего волшебного: это короткая запись для slow = timer(slow). Всё, что дальше, — следствия из этой одной строчки. Код проверен на Python 3.14.5.
Что делает @ на самом деле
Начнём без сахара. Функция timer принимает другую функцию, оборачивает её в wrapper и возвращает обёртку:
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"{func.__name__}: {time.perf_counter() - start:.3f} c")
return result
return wrapper
def slow(n):
return sum(range(n))
slow = timer(slow) # вот здесь и происходит декорирование
slow(3_000_000)
slow: 0.024 c
Последние две строки и есть весь механизм. Имя slow теперь указывает не на исходную функцию, а на wrapper, который внутри себя помнит оригинал. Синтаксис @ заменяет только присваивание:
@timer
def slow(n):
return sum(range(n))
Полностью эквивалентно. Никакой разницы в поведении, только меньше букв и присваивание не отъезжает вниз, под тело функции.
*args, **kwargs в обёртке нужны, чтобы она пропускала через себя любые аргументы. Напиши def wrapper(n), и декоратор станет годен ровно для функций с одним позиционным аргументом.
Декоратор срабатывает при определении, а не при вызове
Об этом спотыкаются чаще всего.
def loud(func):
print(f"декорирую {func.__name__}")
return func
@loud
def never_called():
pass
print("функция так и не вызвана")
декорирую never_called
функция так и не вызвана
Тело loud отработало в момент, когда интерпретатор дошёл до def. Функцию при этом не вызывали ни разу. Отсюда практическое следствие: тяжёлая работа в самом декораторе (открыть файл, сходить в сеть, прочитать конфиг) выполнится на импорте модуля, а не тогда, когда её ждут. Тот же механизм «однажды при def» стоит и за ловушкой изменяемого default-аргумента.
Куда пропадает имя функции
Обёртка подменяет функцию целиком, вместе с её именем и документацией.
@timer
def fetch(url):
"""Скачивает страницу."""
return url
print(fetch.__name__, fetch.__doc__)
wrapper None
Функция называется wrapper, документации нет. Ломается всё, что смотрит на метаданные: help(), автодокументация, логи с именем функции, отладчик. Лечится одной строкой, functools.wraps:
import functools
def timer(func):
@functools.wraps(func) # <-- копирует __name__, __doc__, __module__, __qualname__
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
fetch Скачивает страницу.
wraps кладёт ещё и ссылку на оригинал: fetch.__wrapped__ возвращает недекорированную функцию. Это единственный способ добраться до неё в тестах, когда обёртка мешает.
Правило простое: пишешь декоратор — ставь @functools.wraps(func). Исключений на практике не бывает.
Декоратор с аргументами
Чтобы декоратор сам принимал параметры, нужен ещё один уровень вложенности. @retry(attempts=3) — это вызов, который возвращает декоратор, и уже тот применяется к функции.
def retry(attempts):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for i in range(1, attempts + 1):
try:
return func(*args, **kwargs)
except ValueError as exc:
print(f"попытка {i} упала: {exc}")
raise RuntimeError(f"{func.__name__}: {attempts} попыток исчерпаны")
return wrapper
return decorator
calls = 0
@retry(attempts=3)
def flaky():
global calls
calls += 1
if calls < 3:
raise ValueError("сеть недоступна")
return "ok"
print(flaky())
попытка 1 упала: сеть недоступна
попытка 2 упала: сеть недоступна
ok
Три уровня читаются так: внешний принимает настройки, средний принимает функцию, внутренний принимает аргументы вызова. Каждый возвращает следующий.
Забыть скобки при таком декораторе — классика:
@retry # без (attempts=3)
def broken():
return 1
broken()
TypeError: retry.<locals>.decorator() missing 1 required positional argument: 'func'
Сообщение понятное, если помнить про уровни: retry получил вместо числа саму функцию broken, вернул decorator, и теперь имя broken указывает на decorator, которому при вызове никто не передал func.
Порядок, когда декораторов несколько
Применяются снизу вверх, а исполняются сверху вниз.
def tag(name):
def decorator(func):
@functools.wraps(func)
def wrapper():
return f"<{name}>{func()}</{name}>"
return wrapper
return decorator
@tag("b")
@tag("i")
def text():
return "привет"
print(text())
<b><i>привет</i></b>
Ближайший к def навешивается первым, поэтому i оказался внутри. Мнемоника: читай снизу вверх как последовательность обёртываний. Порядок важен не только для разметки. @app.route над @login_required и наоборот дают разное: в одном случае маршрут регистрируется на защищённую функцию, в другом на открытую.
Декоратор на методе
Метод — обычная функция, self просто приезжает первым позиционным аргументом. Универсальной обёртке с *args вообще ничего менять не надо, а если self нужен явно, его выносят в сигнатуру:
def log_calls(func):
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
print(f"{type(self).__name__}.{func.__name__}{args}")
return func(self, *args, **kwargs)
return wrapper
class Cart:
def __init__(self):
self.items = []
@log_calls
def add(self, name, qty=1):
self.items.append((name, qty))
return len(self.items)
cart = Cart()
print(cart.add("мышь", qty=2))
Cart.add('мышь',)
1
В args попал только 'мышь': qty передали по имени, и он ушёл в kwargs. Мелочь, которая портит логи, если печатать один args и считать, что там все аргументы.
Встроенные декораторы
Часть декораторов в языке уже есть, и на собеседовании спрашивают именно про них.
class Order:
tax = 0.2
def __init__(self, amount):
self._amount = amount
@property
def total(self):
return round(self._amount * (1 + Order.tax), 2)
@staticmethod
def is_valid(amount):
return amount > 0
@classmethod
def free(cls):
return cls(0)
o = Order(1000)
print(o.total, Order.is_valid(-5), Order.free().total)
1200.0 False 0.0
@property превращает метод в вычисляемый атрибут: o.total пишется без скобок. Заодно он становится доступен только на чтение, пока не объявлен сеттер:
o.total = 999
AttributeError: property 'total' of 'Order' object has no setter
@staticmethod — функция, которой не нужны ни объект, ни класс, просто лежит рядом по смыслу. @classmethod получает класс первым аргументом и обычно служит альтернативным конструктором.
Отдельно стоит functools.cache: он запоминает результат для каждого набора аргументов.
@functools.cache
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
| вариант | fib(32) |
|---|---|
| голая рекурсия | 0.135 c |
с @functools.cache | 0.022 мс |
Разница в шесть тысяч раз, и она не про кэш как таковой. Без запоминания fib(32) пересчитывает одни и те же ветки миллионы раз; fib.cache_info() после прогона показывает hits=30, misses=33, то есть каждое значение посчитано ровно однажды.
У cache есть цена: словарь растёт без ограничений и держит ссылки на аргументы. Для чего-то долгоживущего берут @functools.lru_cache(maxsize=1024).
Декоратор классом
Декоратором может быть что угодно вызываемое, в том числе класс с __call__. Так удобнее, когда обёртке нужно хранить состояние.
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.calls = 0
def __call__(self, *args, **kwargs):
self.calls += 1
return self.func(*args, **kwargs)
@CountCalls
def ping():
return "pong"
ping(); ping(); ping()
print(f"{ping.__name__} вызвана {ping.calls} раза")
ping вызвана 3 раза
Состояние лежит в атрибуте объекта, а не в замыкании, и его видно снаружи: ping.calls доступен обычным обращением. Аналог functools.wraps для класса — functools.update_wrapper.
На чём ловят на собеседовании
«Декоратор вызывается каждый раз вместе с функцией». Нет. Сам декоратор отрабатывает один раз, при определении. Каждый вызов проходит через обёртку, которую он вернул. Разницу видно в примере с loud выше.
«@functools.wraps — косметика». Пока не понадобится help(), трассировка с именем функции или подмена в тесте. Ещё это ломает интроспекцию в фреймворках: FastAPI и Pydantic читают аннотации и сигнатуру, а обёртка без wraps подсовывает им (*args, **kwargs).
«@cache потокобезопасен». Сам словарь потокобезопасен, а вот вычисление — нет. Два потока, промахнувшиеся одновременно, посчитают значение дважды: между проверкой и записью управление успевает уйти. Механика этого разобрана в статье про GIL, там же показано, почему проверка перед действием ломается независимо от сборки. Для дорогой функции ставь замок вокруг вычисления сам.
«Декоратор не может испортить функцию». Может, и молча. Забыл return func(...) внутри обёртки — функция начнёт возвращать None, не упав ни разу. Ошибка живучая: тесты на побочные эффекты её не видят.
«Замыкание в декораторе — это глобальная переменная». Каждое применение декоратора создаёт своё замыкание со своими переменными. Две функции под @retry(attempts=3) не делят между собой счётчик попыток.
Частые вопросы
Как коротко ответить, что такое декоратор
Функция, которая принимает функцию и возвращает новую с дополненным поведением, а @ — сахар для f = deco(f). Применяется в момент определения, поэтому нужен functools.wraps, иначе теряются имя и документация. Если декоратору нужны свои параметры, добавляется третий уровень вложенности.
Как декорировать функцию, не меняя её объявление
Обычным присваиванием: slow = timer(slow). Так патчат чужой код, до которого не дотянуться синтаксисом, и так же работают unittest.mock.patch и мониторинговые агенты.
Чем декоратор отличается от контекстного менеджера
Декоратор оборачивает вызов целиком, менеджер — произвольный блок кода. Когда нужны оба, contextlib.ContextDecorator даёт один класс, который работает и как with, и как @.
Можно ли декорировать класс
Да, и это частый приём: декоратор получает класс и возвращает класс. Так устроен @dataclass — он читает аннотации полей и дописывает в класс __init__, __repr__ и __eq__.
Что учить дальше
Замыкания, на которых декораторы стоят, и генераторы: вместе они закрывают половину вопросов про «как устроен Python» на техническом интервью. В пути «Python для продолжающих» на Koddo декораторы разбираются задачами с автопроверкой — сначала логирующая обёртка, потом wraps, потом декоратор с параметрами.
Начните с задачи на декоратор-счётчик вызовов: нужно сохранить отдельное состояние каждой обёртки и не изменить результат исходной функции.