Декораторы в Python: как работают и как написать свой
Практика3 минРедакция Innovialabs
Коротко
Декоратор — это функция, которая принимает функцию и возвращает новую, добавляя поведение до или после вызова; синтаксис @decorator лишь сокращает запись func = decorator(func).
Что такое декоратор
Декоратор — это функция, которая принимает другую функцию и возвращает новую. Запись @timer над функцией — просто короткая форма для func = timer(func). Декораторы позволяют вынести повторяющуюся логику — логирование, замер времени, повторы, проверку прав — из бизнес-кода в одно место.
Работают они благодаря двум свойствам Python: функции — это обычные объекты, и вложенная функция «помнит» переменные внешней (это и есть замыкание).
Шаг 1: замыкание
def make_greeter(greeting):
def greet(name):
return f"{greeting}, {name}!"
return greet
hello = make_greeter("Привет")
print(hello("Анна")) # Привет, Анна!
Внутренняя greet продолжает видеть greeting после того, как make_greeter завершилась. Декоратор устроен так же, только вместо строки во внешнюю функцию передаётся функция.
Шаг 2: первый декоратор и functools.wraps
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"{func.__name__}: {time.perf_counter() - start:.3f} c")
return result
return wrapper
@timer
def build_report(n):
return sum(range(n))
Ключевые детали:
*args, **kwargs— обёртка принимает любые аргументы и передаёт их дальше.return result— без него декорированная функция будет возвращатьNone.functools.wrapsкопирует__name__,__doc__и другие атрибуты оригинала. Без него все функции станут называтьсяwrapper, что ломает логи, отладку и документацию.
Шаг 3: декоратор с аргументами
Если декоратору нужны параметры, добавляется ещё один уровень вложенности: внешняя функция принимает настройки и возвращает сам декоратор.
def retry(times=3, delay=1.0, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except exceptions:
if attempt == times:
raise
time.sleep(delay)
return wrapper
return decorator
@retry(times=5, delay=0.5, exceptions=(ConnectionError,))
def fetch_rates():
...
Обратите внимание: @retry() со скобками и @retry без них — разные вещи. Во втором случае функция попадёт в параметр times, и код сломается.
Практические примеры
Кэширование. Для чистых функций не пишите свой кэш — в стандартной библиотеке есть functools.lru_cache и functools.cache:
@functools.lru_cache(maxsize=256)
def get_tariff(region_id):
...
Аргументы должны быть хешируемыми, а кэш живёт в памяти процесса.
Проверка прав. Декоратор удобно ставить на обработчики, где первым аргументом приходит пользователь:
def require_role(role):
def decorator(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if role not in user.roles:
raise PermissionError(f"Нужна роль {role}")
return func(user, *args, **kwargs)
return wrapper
return decorator
@require_role("admin")
def delete_order(user, order_id):
...
Декораторы-классы и декораторы классов
Это два разных понятия:
| Что | Как выглядит | Когда полезно |
|---|---|---|
| Класс как декоратор | класс с __init__(self, func) и __call__ | нужно хранить состояние, например счётчик вызовов |
| Декоратор класса | функция принимает класс и возвращает класс | регистрация, добавление методов; так работает @dataclass |
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.calls = 0
def __call__(self, *args, **kwargs):
self.calls += 1
return self.func(*args, **kwargs)
Частые ошибки
- Забыть
functools.wraps— теряются имя и docstring. - Забыть
returnв обёртке — функция молча возвращаетNone. - Путать
@decoи@deco()у декораторов с параметрами. - Не учитывать порядок: декораторы применяются снизу вверх, ближайший к
def— первым. - Оборачивать
async defобычной обёрткой: для корутин обёртка тоже должна бытьasyncи делатьawait func(...). - Прятать в декоратор слишком много логики — если поведение неочевидно из названия, читателю кода будет тяжело.