← Назад к списку
ТехническаяPythonMiddle

Как работают декораторы? Напишите пример и объясните, зачем нужен 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() ведут себя по-разному

ИП Кочкин Алексей Сергеевич · ИНН 390509026279 · ОГРНИП 325390000030973 · jiniys2005@yandex.ru