Декораторы с параметрами

Теория

Иногда декоратору нужны настройки: какая роль требуется для доступа, с каким префиксом писать в лог, сколько раз повторять вызов. Тогда пишут фабрику декораторов — функцию, которая принимает параметры и возвращает декоратор. Запись @require_role("manager") выполняется в два шага: сначала вызов require_role("manager") возвращает декоратор, потом этот декоратор применяется к функции. Получается три уровня вложенности: фабрика → декоратор → обёртка, и каждый внутренний уровень помнит параметры внешнего через замыкание.

Когда над функцией несколько декораторов, они применяются снизу вверх (ближайший к def — первым), а при вызове срабатывают сверху вниз: внешняя обёртка вызывает внутреннюю. От порядка зависит поведение: если проверка прав стоит снаружи логирования, отклонённые попытки в лог не попадут. Именно так устроены @app.get("/products") в FastAPI и @permission_required("shop.change_price") в Django.

Наглядно

Таблица 1. Три уровня фабрики декораторов require_role(role).

Уровень Что принимает Что возвращает Когда выполняется
require_role(role) параметр декоратора декоратор один раз, при @require_role("manager")
decorator(func) исходную функцию обёртку один раз, сразу после первого уровня
wrapper(*args, **kwargs) аргументы вызова результат функции (или отказ) при каждом вызове

Таблица 2. Порядок нескольких декораторов на примере @log_call над @require_role("manager").

Порядок Что получается
Применение (при def) снизу вверх: сначала require_role, потом log_call f = log_call(require_role("manager")(f))
Вызов сверху вниз: сначала обёртка log_call, внутри — require_role в лог попадают все попытки, включая отклонённые

На практике

Задача: ограничить изменение цен ролью менеджера и вести журнал всех попыток — с настраиваемыми ролью и префиксом журнала.

Шаг 1. Пишем фабрику декораторов

Внешняя функция принимает роль, внутренние — функцию и аргументы вызова. Пользователь — словарь с ролью:

python
import functools

def require_role(role):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(user, *args, **kwargs):
            if user["role"] != role:
                return {"status": 403, "error": f"нужна роль {role}"}
            return func(user, *args, **kwargs)
        return wrapper
    return decorator

@require_role("manager")
def change_price(user, product_id, price):
    return {"status": 200, "product_id": product_id, "price": price}

manager = {"name": "Ольга", "role": "manager"}
customer = {"name": "Анна", "role": "customer"}
print(change_price(manager, 42, 1599))
print(change_price(customer, 42, 1))

Результат:

вывод
{'status': 200, 'product_id': 42, 'price': 1599}
{'status': 403, 'error': 'нужна роль manager'}

wrapper помнит и role (параметр фабрики), и func — это два уровня замыкания.

Шаг 2. Раскрываем синтаксис

@require_role("manager") — сначала вызов фабрики, потом применение полученного декоратора:

python
def delete_product(user, product_id):
    return {"status": 200, "deleted": product_id}

only_admin = require_role("admin")
delete_product = only_admin(delete_product)
print(delete_product(manager, 42))

Результат:

вывод
{'status': 403, 'error': 'нужна роль admin'}

То же самое одной строкой: delete_product = require_role("admin")(delete_product).

Шаг 3. Делаем параметр необязательным

У фабрики можно задать параметры по умолчанию — но скобки при использовании всё равно нужны:

python
def log_call(prefix="[shop]"):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f"{prefix} вызов {func.__name__}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@log_call()
def calc_total(prices):
    return sum(prices)

@log_call(prefix="[admin]")
def rebuild_index():
    return "ok"

calc_total([1499, 490])
rebuild_index()

Результат:

вывод
[shop] вызов calc_total
[admin] вызов rebuild_index

Шаг 4. Ставим декораторы в нужном порядке

Журнал должен видеть все попытки изменить цену, даже отклонённые, — значит, логирование снаружи:

python
@log_call(prefix="[audit]")
@require_role("manager")
def change_price(user, product_id, price):
    return {"status": 200, "product_id": product_id, "price": price}

print(change_price(customer, 42, 1)["status"])

Результат:

вывод
[audit] вызов change_price
403

Если поменять декораторы местами, require_role вернёт отказ раньше, чем дело дойдёт до журнала, — отклонённая попытка не запишется.

Частые ошибки

  • Фабрика без скобок. @require_role (без ("manager")) передаёт функцию в фабрику как параметр role. Декорированное имя становится декоратором, а вызов возвращает функцию вместо результата.
  • @log_call без скобок при параметрах по умолчанию. Фабрику всё равно нужно вызвать: @log_call().
  • Неверный порядок декораторов. Внешний декоратор срабатывает первым при вызове. Проверка, которая должна отсекать раньше всего (права), или действие, которое должно видеть всё (журнал), — выбирайте место осознанно.
  • Забытый уровень. Фабрика должна вернуть декоратор, декоратор — обёртку; пропуск уровня даёт TypeError при применении или вызове.

Итог

Фабрика декораторов

  • Три уровня: factory(params) → decorator(func) → wrapper(*args, **kwargs).
  • @factory(params) — то же, что f = factory(params)(f).
  • Даже без параметров — со скобками: @factory().

Несколько декораторов

  • Применяются снизу вверх, при вызове срабатывают сверху вниз.
  • Порядок меняет поведение: что снаружи, то видит вызов первым.

Где встречается

  • @require_role(...), @permission_required(...) в Django, @app.get("/path") в FastAPI.

Вопросы на собеседовании

Как написать декоратор с параметрами?

Функцией, которая принимает параметры и возвращает декоратор: def repeat(n): def decorator(func): def wrapper(*a, **kw): ... return wrapper; return decorator. Используется как @repeat(3) — сначала вызывается repeat(3), потом результат применяется к функции.

В каком порядке применяются и вызываются несколько декораторов?

Применяются снизу вверх: @a @b def f равно f = a(b(f)). При вызове первой срабатывает внешняя обёртка a, внутри неё — b, внутри — сама f.

Что будет, если использовать декоратор с параметрами без скобок?

Функция попадёт в фабрику вместо параметра, и имя функции станет указывать на декоратор (результат фабрики). Вызов вернёт обёртку, а не результат — обычно это заметно только по странному поведению, ошибки при определении нет.

Ошибка в тексте

Сообщить об ошибке могут зарегистрированные пользователи — так в сообщениях меньше спама.

Войти, чтобы сообщить