Декораторы и 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__исходной функции — используйте всегда.