Как работают декораторы? Напишите пример и объясните, зачем нужен functools.wraps.
Короткий ответ
- Декоратор — функция, принимающая функцию и возвращающая новую
- @decorator — синтаксический сахар для f = decorator(f)
- Внутри обычно замыкание-обёртка с *args и **kwargs
- functools.wraps сохраняет имя, докстроку и метаданные
- Декоратор с параметрами — это три уровня вложенности
- Применяются для логирования, кэша, ретраев, проверки прав
Декоратор оборачивает функцию дополнительным поведением без изменения её кода, а functools.wraps сохраняет метаданные оригинала.
Как сказать вслух
пример ответаДекоратор — это функция, которая принимает другую функцию и возвращает обёртку вокруг неё. Запись с собачкой — просто сахар для переприсваивания. Обёртка обычно принимает любые аргументы и добавляет поведение до и после вызова: логирование, замер времени, кэш. Обязательно использую functools.wraps, чтобы у обёрнутой функции сохранились имя и докстрока — без этого ломаются отладка и некоторые фреймворки.
Подробный ответ
Основной ответ
Функции в Python — объекты первого класса: их можно передавать и возвращать. Декоратор использует это: принимает функцию, определяет внутри обёртку (замыкание, видящее оригинал), и возвращает её. Запись @log над def func эквивалентна func = log(func) сразу после определения. Декоратор с параметрами добавляет ещё один уровень: фабрика принимает параметры и возвращает декоратор. functools.wraps копирует __name__, __doc__, __module__ и __wrapped__ с оригинала на обёртку — иначе все декорированные функции «превращаются» в wrapper, что ломает интроспекцию, pickle и инструменты вроде Sphinx. Декорировать можно и классы, а декоратором может быть класс с __call__.
Ключевые моменты
- Замыкание. Обёртка хранит ссылку на оригинальную функцию через замыкание, поэтому декоратор работает без глобального состояния.
- functools.wraps. Сохраняет метаданные оригинала; без него func.__name__ вернёт 'wrapper', а help() покажет не ту докстроку.
- Декоратор с аргументами. @retry(times=3) — вызов фабрики, которая возвращает настоящий декоратор; итого три вложенные функции.
- Порядок применения. Декораторы применяются снизу вверх: ближайший к функции оборачивает первым.
Практический контекст
Декораторы повсюду: @app.get в FastAPI, @login_required в Django, @pytest.fixture, @lru_cache, @property. На собеседовании часто просят написать декоратор с нуля — замер времени или ретраи. Интервьюер смотрит, напишет ли кандидат *args/**kwargs, вернёт ли результат из обёртки и вспомнит ли про wraps. Сильный ответ включает декоратор с параметрами.
Пример кода
import functools, time
def timed(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}s")
return wrapper
@timed
def slow_sum(n):
return sum(range(n))
slow_sum(10**6)Частые ошибки
- Забывают вернуть результат оригинальной функции из обёртки
- Не используют functools.wraps, теряя имя и докстроку функции
- Путают декоратор и его вызов: @retry против @retry() ведут себя по-разному