Урок курса
Декораторы в Python: функции, @ и параметры
Python для профессионаловДекоратор принимает вызываемый объект и возвращает другой вызываемый объект. Это позволяет добавить логирование, проверку прав, кэширование или измерение времени, не смешивая служебную логику с телом функции.
from functools import wraps
def traced(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"call {func.__name__}")
return func(*args, **kwargs)
return wrapper
@traced
def add(a: int, b: int) -> int:
return a + b
Запись @traced эквивалентна add = traced(add). Декоратор выполняется в момент определения функции, а wrapper — при каждом вызове.
Почему нужен functools.wraps
Без @wraps(func) наружу торчит имя и документация обёртки. Это мешает логам, introspection, генерации документации и некоторым фреймворкам. wraps копирует важные метаданные и сохраняет ссылку __wrapped__ на исходную функцию.
Обёртка почти всегда принимает *args, **kwargs, чтобы не ломать разные сигнатуры. При этом типизацию сложных универсальных декораторов лучше оформлять через ParamSpec и TypeVar; простая аннотация Callable[..., Any] скрывает ошибки аргументов.
Порядок нескольких декораторов важен. В записи @a над @b сначала создаётся b(func), затем a(...). При вызове внешняя обёртка a срабатывает первой.
Декоратор с параметрами
Если у декоратора есть настройка, появляется ещё один уровень функции — фабрика декораторов.
from functools import wraps
def repeat(times: int):
if times < 1:
raise ValueError("times must be positive")
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
Теперь @repeat(3) сначала вызывает фабрику, получает декоратор и только затем оборачивает функцию. Не прячьте в декоратор критичное поведение без явного имени: транзакции, retry и авторизация должны оставаться наблюдаемыми и тестируемыми.
Как wraps сохраняет имя обёрнутой функции
Без functools.wraps декорированная функция часто выглядит для диагностики как wrapper. С wraps имя и документация сохраняются. Ниже декоратор считает вызовы; аргументы передаются дальше через *args и **kwargs, а результат исходной функции возвращается без изменения.
from functools import wraps
def counted(func):
@wraps(func)
def wrapper(*args, **kwargs):
wrapper.calls += 1
return func(*args, **kwargs)
wrapper.calls = 0
return wrapper
@counted
def add(a, b=1):
return a+b
assert add(2, b=3) == 5
assert add.__name__ == "add" and add.calls == 1
print(add.__name__, add.calls)
Счётчик здесь хранится на одной общей обёртке и предназначен для демонстрации, а не для конкурентной production-метрики. Проверьте, не потеряли ли вы return, именованные аргументы и исключения при добавлении своей логики. Поведение functools.wraps.
Продолжите разбор: Генераторы и yield в Python: как это работает.
Задача 1
Зачем обычно добавляют @functools.wraps к wrapper-функции?
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
