Декораторы с параметрами
Теория
Иногда декоратору нужны настройки: какая роль требуется для доступа, с каким префиксом писать в лог, сколько раз повторять вызов. Тогда пишут фабрику декораторов — функцию, которая принимает параметры и возвращает декоратор. Запись @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. Пишем фабрику декораторов
Внешняя функция принимает роль, внутренние — функцию и аргументы вызова. Пользователь — словарь с ролью:
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") — сначала вызов фабрики, потом применение полученного декоратора:
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. Делаем параметр необязательным
У фабрики можно задать параметры по умолчанию — но скобки при использовании всё равно нужны:
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. Ставим декораторы в нужном порядке
Журнал должен видеть все попытки изменить цену, даже отклонённые, — значит, логирование снаружи:
@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.
Что будет, если использовать декоратор с параметрами без скобок?
Функция попадёт в фабрику вместо параметра, и имя функции станет указывать на декоратор (результат фабрики). Вызов вернёт обёртку, а не результат — обычно это заметно только по странному поведению, ошибки при определении нет.