Декораторы и functools.wraps

Один из самых частых продвинутых вопросов: что такое декоратор и как он работает изнутри.

Декоратор — это функция, которая принимает функцию и возвращает новую функцию, расширяющую поведение исходной. Синтаксис @decorator — сахар над func = decorator(func).

Вопрос: что делает @decorator?

Чёткий ответ. Запись @log над функцией greet эквивалентна greet = log(greet). Декоратор оборачивает исходную функцию во вложенную wrapper, которая что-то делает до/после вызова.

def log(func):
    def wrapper(*args, **kwargs):
        print(f"вызов {func.__name__}")
        result = func(*args, **kwargs)
        print(f"готово: {result}")
        return result
    return wrapper

@log                       # greet = log(greet)
def greet(name):
    return f"Привет, {name}"

print(greet("Аня"))

Вывод:

вызов greet
готово: Привет, Аня
Привет, Аня

Проблема: декоратор «съедает» имя и докстроку

Без защиты обёртка подменяет метаданные: __name__ и __doc__ станут от wrapper, а не от исходной функции. Это ломает интроспекцию и документацию.

def log(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@log
def greet(name):
    """Приветствует пользователя."""
    return f"Привет, {name}"

print(greet.__name__)      # ожидали greet
print(greet.__doc__)       # ожидали докстроку

Вывод:

wrapper
None

Решение: functools.wraps

@functools.wraps(func) копирует метаданные исходной функции на обёртку. Это стандартная практика — всегда используйте его в декораторах.

import functools

def log(func):
    @functools.wraps(func)             # сохраняем имя и докстроку
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@log
def greet(name):
    """Приветствует пользователя."""
    return f"Привет, {name}"

print(greet.__name__)
print(greet.__doc__)

Вывод:

greet
Приветствует пользователя.

Декоратор с аргументами

Частый следующий вопрос: как сделать декоратор с параметром, например @repeat(3)? Нужен ещё один уровень вложенности: внешняя функция принимает аргумент и возвращает обычный декоратор. То есть @repeat(3) — это сначала вызов repeat(3), который возвращает декоратор, а уже он оборачивает функцию.

import functools

def repeat(times):                         # принимает параметр
    def decorator(func):                   # обычный декоратор
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    print(f"Привет, {name}")

greet("Аня")

Вывод:

Привет, Аня
Привет, Аня
Привет, Аня

Три уровня функций: repeat запоминает times, decorator получает функцию, wrapper вызывает её нужное число раз. Запись @repeat(3) читается как greet = repeat(3)(greet).

Итог

  • Декоратор — функция, оборачивающая функцию; @d = f = d(f).
  • Обёртка обычно принимает *args, **kwargs, чтобы работать с любой сигнатурой.
  • Декоратор с аргументом — это функция, возвращающая декоратор: @d(arg) = f = d(arg)(f).
  • functools.wraps сохраняет __name__/__doc__ исходной функции — используйте всегда.
Проверьте себя
1. Чему эквивалентен @log над функцией greet?
Agreet()
Bgreet = log(greet)
Clog = greet(log)
Ddel greet
2. Зачем в декораторе нужен functools.wraps?
AУскоряет функцию
BСохраняет __name__ и __doc__ исходной функции на обёртке
CДелает функцию приватной
DКеширует результат
3. Почему wrapper обычно принимает *args, **kwargs?
AДля скорости
BЧтобы оборачивать функции с любой сигнатурой
CЭто обязательно по синтаксису
DЧтобы избежать рекурсии
Поддержать проект