Декораторы в Python: что это, как работают и как написать свой
Декоратор в Python — это функция, которая принимает другую функцию и возвращает новую, обычно обёртку, добавляющую поведение до или после вызова: логирование, замер времени, кэширование, проверку прав. Запись @decorator над def — сокращение для func = decorator(func); чтобы обёртка сохранила имя и docstring исходной функции, используют functools.wraps.
Декоратор в Python — это функция, которая принимает функцию и возвращает другую функцию, как правило обёртку вокруг исходной. Так к функции добавляют поведение — логирование, замер времени, кэш, повторные попытки — не меняя её код. Декораторы в Python пишутся строкой @имя над объявлением функции и встречаются повсюду: во Flask и FastAPI, в pytest, в стандартной библиотеке.
Что такое декоратор простыми словами
Представьте подарок и обёрточную бумагу. Подарок (функция) остаётся тем же, но снаружи появляется слой, который срабатывает первым. Декоратор и есть такая упаковка: при вызове сначала выполняется код обёртки, она вызывает исходную функцию и может что-то сделать с результатом.
Работает это потому, что в Python функции — обычные объекты. Их можно передавать в другие функции, возвращать из функций и объявлять внутри функций. На этих трёх свойствах и построены декораторы.
Как работает декоратор: @ — это синтаксический сахар
Возьмём простейший декоратор, который печатает сообщение до и после вызова:
def log_calls(func):
def wrapper():
print(f"Вызываю {func.__name__}")
result = func()
print(f"{func.__name__} завершилась")
return result
return wrapper
@log_calls
def hello():
print("Привет!")
hello()
Вызываю hello
Привет!
hello завершилась
Запись @log_calls над def полностью эквивалентна строке после объявления:
def hello():
print("Привет!")
hello = log_calls(hello)
Имя hello теперь указывает на wrapper, а исходная функция живёт внутри замыкания — wrapper помнит переменную func из внешней функции. Важная деталь: декоратор применяется один раз, в момент выполнения def, то есть при импорте модуля, а обёртка срабатывает при каждом вызове.
Замыкания и декораторы
Замыкание — вложенная функция, которая запоминает переменные внешней функции после её завершения. Без замыканий декораторов бы не было: именно так wrapper получает доступ к func. В замыкании можно хранить и состояние, например счётчик вызовов:
def count_calls(func):
calls = 0
def wrapper(*args, **kwargs):
nonlocal calls
calls += 1
print(f"{func.__name__}: вызов №{calls}")
return func(*args, **kwargs)
return wrapper
Как написать декоратор в Python: универсальный шаблон
У первого примера есть два недостатка: обёртка не принимает аргументы и «съедает» имя исходной функции. Рабочий шаблон, с которого стоит начинать любой декоратор, выглядит так:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# код до вызова
result = func(*args, **kwargs)
# код после вызова
return result
return wrapper
Три правила шаблона:
*args, **kwargs— обёртка принимает любые аргументы и передаёт их дальше без изменений.return result— не забыть вернуть результат, иначе декорированная функция всегда будет отдаватьNone.@functools.wraps(func)— перенести метаданные исходной функции на обёртку.
Пример декоратора — замер времени выполнения:
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} заняла {elapsed:.3f} с")
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
slow_sum(10_000_000) # slow_sum заняла 0.142 с
Зачем нужен functools.wraps
Без wraps у декорированной функции подменяются имя, docstring и другие атрибуты — снаружи виден wrapper:
def plain(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@plain
def add(a, b):
"""Складывает два числа."""
return a + b
print(add.__name__, add.__doc__) # wrapper None
С @functools.wraps(func) обёртка получает __name__, __doc__, __module__, __qualname__, __annotations__ исходной функции, а в атрибуте __wrapped__ хранится ссылка на оригинал. Это важно для отладки и трейсбеков, для help(), для фреймворков, которые регистрируют функции по имени, и для inspect.signature, который показывает настоящую сигнатуру.
Декоратор с параметрами (с аргументами)
Иногда декоратору нужны настройки: @retry(times=3), @app.route("/"). Для этого добавляют ещё один уровень вложенности. Внешняя функция принимает параметры и возвращает настоящий декоратор — такую конструкцию называют фабрикой декораторов.
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def ping():
print("ping")
ping() # ping, ping, ping — три строки
Порядок вызовов: repeat(times=3) возвращает decorator, тот применяется к ping и возвращает wrapper. Иначе говоря, @repeat(times=3) — это ping = repeat(times=3)(ping).
Практичный пример — повтор при ошибке:
import functools
import time
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_rate():
...
Несколько декораторов на одной функции
Декораторы можно ставить друг над другом. Применяются они снизу вверх: ближний к def — первым.
@timer
@log_calls
def report():
...
# то же самое:
report = timer(log_calls(report))
При вызове внешний слой (timer) срабатывает первым, поэтому он замерит и время работы log_calls. Порядок имеет значение, например, если один декоратор проверяет права доступа, а другой кэширует результат.
Стандартные и встроенные декораторы Python
Многие полезные декораторы уже есть в языке и стандартной библиотеке:
| Декоратор | Что делает |
|---|---|
@property | превращает метод в атрибут только для чтения; с @x.setter — в управляемый атрибут |
@staticmethod | метод класса без self, обычная функция в пространстве имён класса |
@classmethod | метод получает класс cls вместо экземпляра — так пишут альтернативные конструкторы |
@functools.cache, @functools.lru_cache | кэшируют результаты по аргументам |
@functools.wraps | копирует метаданные функции на обёртку |
@dataclasses.dataclass | генерирует __init__, __repr__, __eq__ для класса |
@contextlib.contextmanager | делает из генератора менеджер контекста для with |
Кэширование: functools.cache и lru_cache
Кэширующий декоратор запоминает результат для каждого набора аргументов и при повторном вызове возвращает его без вычисления. Классический пример — рекурсивные числа Фибоначчи, которые без кэша считаются экспоненциально долго:
import functools
@functools.cache
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(100)) # 354224848179261915075
functools.cache (появился в Python 3.9) хранит все результаты без ограничений. @functools.lru_cache(maxsize=128) держит только последние записи и вытесняет старые — это безопаснее для долго работающих сервисов. Статистику смотрят через fib.cache_info(), сбрасывают — через fib.cache_clear(). Аргументы таких функций должны быть хешируемыми: список в качестве аргумента вызовет TypeError: unhashable type: 'list'. И кэшировать стоит только чистые функции — те, что при одинаковых аргументах всегда возвращают одно и то же и не имеют побочных эффектов.
Что делает декоратор property
@property позволяет обращаться к методу как к атрибуту, без скобок, и при необходимости контролировать присваивание:
class Account:
def __init__(self, balance):
self._balance = balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError("Баланс не может быть отрицательным")
self._balance = value
acc = Account(100)
print(acc.balance) # 100
acc.balance = -5 # ValueError
staticmethod и classmethod
class Money:
def __init__(self, kopecks):
self.kopecks = kopecks
@classmethod
def from_rubles(cls, rubles):
return cls(round(rubles * 100))
@staticmethod
def is_valid(value):
return value >= 0
m = Money.from_rubles(12.5)
print(m.kopecks, Money.is_valid(-1)) # 1250 False
Именно @classmethod позволяет методу работать с самим классом, а не с его экземплярами: from_rubles корректно создаст и объект подкласса.
Декораторы классов и классы-декораторы
Декоратор можно применить к классу: он получает класс и возвращает класс (тот же или изменённый). Так устроен @dataclass. Простой пример — реестр классов:
registry = {}
def register(cls):
registry[cls.__name__] = cls
return cls
@register
class CsvExporter:
pass
print(registry) # {'CsvExporter': <class '__main__.CsvExporter'>}
Обратная ситуация — класс в роли декоратора. Подойдёт любой объект с методом __call__; состояние удобно хранить в атрибутах экземпляра:
import functools
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)
@CountCalls
def tick():
pass
tick(); tick()
print(tick.calls) # 2
Асинхронный декоратор
Для async def-функций обёртка тоже должна быть корутиной и ждать результат через await, иначе вернётся невыполненный объект корутины:
import functools
def log_async(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
print(f"Старт {func.__name__}")
result = await func(*args, **kwargs)
print(f"Готово {func.__name__}")
return result
return wrapper
Зачем нужны декораторы в Python
- Сквозная логика — логирование, метрики, замер времени без копирования кода в каждую функцию.
- Кэширование —
@functools.cacheдля дорогих чистых вычислений. - Проверки — права доступа, валидация аргументов, авторизация в веб-обработчиках.
- Регистрация — маршруты во Flask и FastAPI (
@app.get("/")), фикстуры pytest. - Надёжность — повторные попытки, таймауты, обработка ошибок.
Главный плюс — разделение ответственности: функция занимается своей задачей, а вспомогательное поведение подключается одной строкой и так же легко убирается.
Типичные ошибки
- Обёртка не возвращает результат — функция после декорирования отдаёт
None. - Нет
*args, **kwargs—TypeErrorпри вызове с аргументами. - Забыли
wraps— неверные__name__и docstring, путаница в логах. - Скобки у фабрики —
@repeatвместо@repeat(3): функция попадёт в параметрtimes. - Тяжёлый код в теле декоратора — он выполняется при импорте модуля, а не при вызове.
Итог
Декораторы в Python — это функции высшего порядка: принимают функцию, возвращают обёртку, а @ — лишь короткая запись присваивания. Начинайте с шаблона с *args, **kwargs, return result и functools.wraps, для настроек добавляйте уровень фабрики. Функции, замыкания и аннотации, на которых держатся декораторы, разобраны на практике в курсе по Python.
Частые вопросы
Можно ли снять декоратор с функции?
Если декоратор использует functools.wraps, оригинал доступен через атрибут __wrapped__: func.__wrapped__(...) вызовет функцию без обёртки. Иначе остаётся только убрать строку с @ из кода.
Когда выполняется код декоратора?
Тело самого декоратора выполняется один раз, при объявлении функции, обычно во время импорта модуля. Код внутри wrapper выполняется при каждом вызове декорированной функции.
Можно ли декорировать методы класса?
Да, обычный декоратор с *args, **kwargs работает и с методами: self просто окажется первым элементом args.
Чем декоратор отличается от наследования?
Наследование меняет поведение класса через иерархию, а декоратор оборачивает конкретную функцию или класс снаружи и легко комбинируется с другими декораторами без изменения кода.
Эта тема подробно разобрана с практикой в курсе «Python 3.15: от новичка до профи».
Перейти к курсу