Декораторы в Python: от логгера до контроля доступа
Разбираем, как декоратор из синтаксического сахара превращается в рабочий инструмент: логирование, замер времени, кеш и проверка прав. С разбором того, что ломается при неаккуратной обёртке.

Декоратор в Python — это функция, которая принимает другую функцию и возвращает новую, обычно с добавленным поведением. Синтаксис @wrapper над определением функции — то же самое, что func = wrapper(func), только читаемее. За этим простым механизмом прячется целый класс задач: логирование, замер времени, кеширование, повторные попытки и контроль доступа. Здесь — четыре рабочих приёма от простого логгера до проверки прав, и разбор того, где наивная реализация даёт сбой. Всё на Python 3.10+, стандартная библиотека.
Базовая форма: почему нужен functools.wraps
Начнём с логгера, который печатает имя функции и её аргументы перед вызовом. Очевидное решение выглядит так:
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"Вызов {func.__name__} с args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} вернул {result!r}")
return result
return wrapper
@log_calls
def add(a, b):
"""Складывает два числа."""
return a + b
add(2, 3)
Работает, но есть скрытая проблема. После декорирования add.__name__ вернёт 'wrapper', а add.__doc__ станет None. Оригинальные метаданные потеряны — это ломает интроспекцию, автодокументацию и отладку. Лечится одной строкой: @functools.wraps(func) над wrapper.
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Вызов {func.__name__} с args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} вернул {result!r}")
return result
return wrapper
functools.wraps копирует __name__, __doc__, __module__, __qualname__ и __dict__, а также прописывает __wrapped__ — ссылку на оригинал. Через неё можно достать необёрнутую функцию. Правило простое: любой декоратор без wraps — заготовка на будущую боль.
Декоратор без functools.wraps — как переименование файла без сохранения расширения: работать вроде работает, пока кто-нибудь не попробует разобраться, что это вообще было.
Декоратор с параметрами: замер времени с порогом
Часто нужно не просто обернуть функцию, а настроить обёртку. Например, логировать только вызовы дольше заданного порога. Тогда появляется третий уровень вложенности: внешняя функция принимает параметры и возвращает сам декоратор.
import functools
import time
def timed(threshold_ms=0):
"""Логирует вызов, если он занял дольше threshold_ms миллисекунд."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed_ms = (time.perf_counter() - start) * 1000
if elapsed_ms >= threshold_ms:
print(f"{func.__name__}: {elapsed_ms:.1f} мс")
return result
return wrapper
return decorator
@timed(threshold_ms=100)
def load_report():
time.sleep(0.2)
return "готово"
load_report()
Обратите внимание: @timed(threshold_ms=100) — это вызов, который возвращает декоратор. Скобки обязательны. Частая ошибка новичков — написать @timed без скобок; тогда func попадёт в параметр threshold_ms, и всё развалится с невнятной ошибкой. Если хотите поддержать оба варианта — с параметрами и без, — придётся проверять первый аргумент на вызываемость, и это заметно усложняет код. Проще выбрать одну форму и держаться её.
Кеширование: не изобретайте, берите готовое
Кеш результатов — классическая задача для декоратора, но писать её вручную почти никогда не нужно. В стандартной библиотеке есть functools.lru_cache и, начиная с Python 3.9, functools.cache — упрощённая обёртка без ограничения размера.
import functools
@functools.lru_cache(maxsize=128)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
fib(30)
print(fib.cache_info()) # hits, misses, maxsize, currsize
У готового декоратора есть то, что в самописном обычно забывают: cache_info() для диагностики и cache_clear() для сброса. Ограничение — аргументы должны быть хешируемыми. Список или словарь в аргументах уронят вызов с TypeError: unhashable type. И помните: кеш живёт на уровне функции для всего процесса, поэтому мутабельные значения по умолчанию и объекты с состоянием кешировать опасно.
Контроль доступа: декоратор как ворота
Теперь к тому, ради чего декораторы часто и вспоминают, — проверка прав. Идея: перед вызовом функции убедиться, что текущий пользователь имеет нужную роль, иначе бросить исключение. Ключевой момент — декоратор не должен сам знать, откуда берётся пользователь; он получает это через передаваемую функцию или контекст.
import functools
class AccessDenied(Exception):
pass
def require_role(*allowed_roles):
"""Пропускает вызов, только если у пользователя есть одна из ролей."""
def decorator(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if user.role not in allowed_roles:
raise AccessDenied(
f"Роль {user.role!r} не входит в {allowed_roles}"
)
return func(user, *args, **kwargs)
return wrapper
return decorator
class User:
def __init__(self, role):
self.role = role
@require_role("admin", "editor")
def delete_article(user, article_id):
return f"Статья {article_id} удалена пользователем {user.role}"
print(delete_article(User("admin"), 42))
# delete_article(User("reader"), 42) поднимет AccessDenied
Здесь wrapper явно ждёт первым аргументом user. Это осознанный контракт: функция и её декоратор договариваются о том, где искать субъект проверки. В веб-фреймворках роль этого user играет объект запроса. Например, в Django декоратор доступа вытаскивает пользователя из request.user — там уже есть готовый @login_required и @permission_required из django.contrib.auth.decorators, и в реальном проекте разумнее опираться на них, а не писать свой.
Когда что выбирать
| Задача | Решение | Что учесть |
|---|---|---|
| Логирование, замер времени | Свой декоратор с wraps | Не забыть параметр-порог, если логов много |
| Кеш результатов | functools.lru_cache / cache | Аргументы должны быть хешируемыми |
| Повторные попытки | Свой декоратор или библиотека tenacity | Ограничить число попыток и бэкофф |
| Проверка прав в вебе | Штатные декораторы фреймворка | Не дублировать логику аутентификации |
Что ломается
Даже аккуратный декоратор спотыкается на нескольких вещах. Держите их в голове.
- Порядок наложения. Декораторы применяются снизу вверх: тот, что ближе к функции, оборачивается первым.
@log_callsнад@timed()и наоборот дадут разный порядок вывода. Ошибка тут тихая — код работает, но логирует не то. - Потеря сигнатуры для инструментов.
functools.wrapsчинит__name__и__doc__, но не саму сигнатуру для всех случаев. Если инструменту важна точная сигнатура (например, некоторые валидаторы), смотрите в сторону библиотекиwrapt, которая сохраняет её честнее. - Декорирование методов класса. Ваш
*argsвwrapperдля метода первым получитself. Обычно это нормально, но при проверке прав легко случайно принятьselfзаuser. Проверяйте, что именно приходит первым аргументом. - Состояние в замыкании. Если декоратор хранит счётчик или кеш в переменной внешней функции, это состояние общее для всех вызовов в процессе. В многопоточной среде без блокировки получите гонку.
Что дальше
Если пишете декораторы часто и вам важна честная сигнатура и корректная работа с методами и дескрипторами — посмотрите библиотеку wrapt: она решает те граничные случаи, которые голый functools.wraps оставляет открытыми. Для повторных попыток с бэкоффом есть tenacity. А прежде чем писать свой контроль доступа — проверьте, что штатные средства фреймворка не закрывают задачу: чаще всего закрывают, и своя реализация просто добавляет поверхность для ошибок безопасности.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: functools — Higher-order functions and operations on callable objects (Python docs), PEP 318 — Decorators for Functions and Methods
Частые вопросы
Зачем нужны скобки в @timed(), если у @log_calls их нет?
А если у меня Python 3.8 — сработает functools.cache?
Почему после декоратора help() показывает не мою документацию?
Можно ли навешивать несколько декораторов на одну функцию?
Стоит ли писать свой декоратор проверки прав для веб-приложения?
Что делать, если lru_cache падает с TypeError: unhashable type?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.