История вопроса
Декораторы — один из мощнейших инструментов Python, позволяющий модифицировать поведение функций или методов. Иногда нужно не просто "обернуть" функцию, а настроить декоратор с помощью параметров (аргументов). Эти случаи встречаются при логировании, проверках времени, ограничениях доступа и т.п.
Проблема
Обычные декораторы принимают только одну функцию, которую нужно обернуть. Когда требуется передать самому декоратору параметры, синтаксис становится сложнее, что часто приводит к ошибкам, особенно при вложенности функций и пробросе *args/**kwargs.
Решение
Декоратор с параметрами реализуется функцией более высокого порядка: сначала вызывается внешняя "декорирующая" функция с аргументами, которая создаёт и возвращает сам декоратор, а декоратор уже получает функцию и возвращает обёртку:
def repeat(n): def decorator(func): def wrapper(*args, **kwargs): result = None for _ in range(n): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def greet(name): print(f"Hello, {name}!") greet("Python") # Вывод: Hello, Python! (3 раза)
Ключевые особенности:
Почему нельзя реализовать декоратор с параметрами так же, как обычный декоратор?
Если вы применяете @decorator, Python передаёт функцию в качестве аргумента декоратору. Если добавляете скобки (@decorator()), Python сначала вызывает функцию, и только её результат интерпретируется как декоратор.
def deco(func): # обычный декоратор: @deco def deco_with_args(arg): # декоратор с аргументом: @deco_with_args(arg)
Чем принципиально отличаются декораторы с аргументами от декораторов без аргументов на уровне вызова?
Декораторы без аргументов получают на вход функцию, декораторы с аргументами — не функцию, а параметры, и возвращают из себя декоратор.
Как правильно применять functools.wraps и зачем это делать?
functools.wraps(func) в обёртке сохраняет имя, строку документации и другие метаданные оригинальной функции, иначе все эти сведения заменятся у wrapper, что мешает отладке и introspection.
import functools def deco_with_args(arg): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return decorator
Реализовали декоратор без учёта параметров или с неверным количеством вложенных функций:
def log(level): def wrapper(func): # ошибка — wrapper должен быть глубже print(f"Log: {level}") func() # Функция не возвращается как декоратор return wrapper @log("INFO") def action(): print("Work!")
Плюсы:
Минусы:
Использование functools.wraps и корректных вложенных функций:
import functools def timer(units): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): import time start = time.time() result = func(*args, **kwargs) end = time.time() if units == 'ms': duration = (end - start) * 1000 else: duration = end - start print(f"Duration: {duration:.4f} {units}") return result return wrapper return decorator @timer('ms') def op(): sum(range(1000)) op()
Плюсы:
Минусы: