Декораторы в Python
Декораторы в Python: синтаксис @, обёртка с *args и **kwargs, functools.wraps, декоратор с параметрами, порядок применения, cache, примеры и частые ошибки.
Содержание · 11 разделов
- На чём держатся декораторы
- Универсальный декоратор: *args, **kwargs и return
- functools.wraps: сохранить имя и docstring
- Декоратор с параметрами
- Пример: повтор при ошибке (retry)
- Несколько декораторов: порядок применения
- Логирование вызовов
- Готовые декораторы
- Стандартная библиотека
- Django и FastAPI
- Декоратор-класс
- Частые ошибки
- wrapper не возвращает результат
- Декоратор не возвращает wrapper
- Декоратор с параметрами без скобок
- Потерянное имя функции
- Обёртка принимает фиксированные аргументы
- Частые вопросы
- Как вызвать исходную функцию без декоратора?
- Как написать декоратор для async-функции?
- Как сделать декоратор, который работает и со скобками, и без?
- Можно ли декорировать класс?
- Как применить декоратор к функции из чужой библиотеки?
- Итог
Декоратор в Python — это функция, которая принимает другую функцию и возвращает новую, обычно с дополнительным поведением. Запись @decorator над def — сокращение для func = decorator(func). Короткий ответ:
def loud(func):
def wrapper():
print("До вызова")
func()
print("После вызова")
return wrapper
@loud
def hello():
print("Привет!")
hello()
До вызова
Привет!
После вызова
Декораторы позволяют добавить к функции логирование, замер времени, кэширование или проверку прав, не меняя её код. Ниже — как они устроены, как правильно написать свой декоратор, как передать ему параметры и какие декораторы уже есть в стандартной библиотеке, Django и FastAPI. Примеры проверены на Python 3.12.
На чём держатся декораторы
Декораторы возможны благодаря двум свойствам функций, подробно разобранным в статье о функциях в Python:
- Функция — обычный объект. Её можно передать в другую функцию как аргумент и вернуть как результат.
- Замыкание. Вложенная функция помнит переменные объемлющей функции —
wrapperпомнитfuncдаже после того, какloudзавершилась.
Без синтаксиса @ тот же код выглядит так:
def hello():
print("Привет!")
hello = loud(hello) # то же, что @loud над def
hello()
print(hello.__name__) # → wrapper
Имя hello теперь указывает на wrapper, а исходная функция доступна только внутри замыкания. Последняя строка показывает побочный эффект, к которому мы вернёмся ниже.
Универсальный декоратор: *args, **kwargs и return
Декоратор loud работает только с функциями без аргументов и теряет возвращаемое значение. Правильный шаблон принимает любые аргументы через *args и **kwargs и возвращает результат исходной функции:
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:.2f} с")
return result
return wrapper
@timer
def slow_sum(a, b, delay=0.5):
time.sleep(delay)
return a + b
print(slow_sum(2, 3))
slow_sum выполнилась за 0.50 с
5
Время на вашей машине может немного отличаться. Этот шаблон — основа почти любого декоратора: получить функцию, объявить wrapper, вызвать в нём func(*args, **kwargs), вернуть результат, вернуть wrapper.
functools.wraps: сохранить имя и docstring
Без @functools.wraps декорированная функция «забывает» своё имя и описание:
def timer(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@timer
def slow_sum(a, b):
"""Сложить два числа."""
return a + b
print(slow_sum.__name__, slow_sum.__doc__) # → wrapper None
С @functools.wraps(func) над wrapper вывод станет slow_sum Сложить два числа.. Декоратор wraps копирует __name__, __doc__, __module__ и другие атрибуты и добавляет __wrapped__ — ссылку на исходную функцию. Это важно для отладки, документации, help() и фреймворков, которые читают имя и сигнатуру функции. Используйте wraps всегда.
Декоратор с параметрами
Чтобы передать декоратору настройки — @repeat(times=3), — нужен ещё один уровень вложенности: внешняя функция принимает параметры и возвращает настоящий декоратор:
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(host):
print("ping", host)
ping("example.com")
ping example.com
ping example.com
ping example.com
Запись @repeat(times=3) сначала вызывает repeat(3), получает decorator, и уже он применяется к ping.
Пример: повтор при ошибке (retry)
import functools
def retry(attempts=3, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
print(f"Попытка {attempt} не удалась: {e}")
if attempt == attempts:
raise
return wrapper
return decorator
calls = 0
@retry(attempts=3, exceptions=(ConnectionError,))
def fetch():
global calls
calls += 1
if calls < 3:
raise ConnectionError("сервер не отвечает")
return "данные"
print(fetch())
Попытка 1 не удалась: сервер не отвечает
Попытка 2 не удалась: сервер не отвечает
данные
Перехватываются только перечисленные типы исключений, а после последней попытки ошибка пробрасывается дальше через raise. Как работают try/except и raise, описано в статье об обработке исключений.
Несколько декораторов: порядок применения
Декораторы применяются снизу вверх — ближайший к def первым:
import functools
def bold(func):
@functools.wraps(func)
def wrapper():
return "<b>" + func() + "</b>"
return wrapper
def italic(func):
@functools.wraps(func)
def wrapper():
return "<i>" + func() + "</i>"
return wrapper
@bold
@italic
def text():
return "Привет"
print(text()) # → <b><i>Привет</i></b>
Это эквивалентно text = bold(italic(text)): italic оборачивает функцию первым, а bold — снаружи. При вызове внешний декоратор срабатывает первым.
Логирование вызовов
Ещё один типичный пример — печать аргументов и результата:
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
params = [repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()]
print(f"→ {func.__name__}({', '.join(params)})")
result = func(*args, **kwargs)
print(f"← {func.__name__} вернула {result!r}")
return result
return wrapper
@log_calls
def greet(name, punctuation="!"):
return f"Привет, {name}{punctuation}"
greet("Анна", punctuation="?")
→ greet('Анна', punctuation='?')
← greet вернула 'Привет, Анна?'
Готовые декораторы
Стандартная библиотека
| Декоратор | Что делает |
|---|---|
@property | метод, который читается как атрибут |
@classmethod | метод получает класс cls вместо объекта |
@staticmethod | метод без self и cls |
@functools.cache | запоминает результаты вызовов без ограничения размера (3.9+) |
@functools.lru_cache(maxsize=128) | кэш с вытеснением давно не использованных значений |
@functools.wraps(func) | переносит имя и docstring на обёртку |
@dataclasses.dataclass | генерирует __init__, __repr__, __eq__ для класса |
@contextlib.contextmanager | превращает генератор в контекстный менеджер для with |
Первые три подробно разобраны в статье о классах и ООП в Python. Кэш наглядно ускоряет рекурсию:
from functools import cache
@cache
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(80)) # → 23416728348467685
print(fib.cache_info()) # → CacheInfo(hits=78, misses=81, maxsize=None, currsize=81)
Без @cache такой вызов пересчитывал бы одни и те же значения экспоненциальное число раз. Кэшировать стоит только функции, результат которых зависит лишь от аргументов, а аргументы должны быть хешируемыми.
Django и FastAPI
Во фреймворках декораторы встречаются на каждом шаге:
- Django:
@login_requiredизdjango.contrib.auth.decoratorsзакрывает представление от анонимных пользователей,@admin.register(Article)регистрирует модель в админке — см. руководство по Django Admin. - FastAPI:
@app.get("/items/")связывает функцию с маршрутом — подробнее в руководстве по FastAPI.
Декоратор вроде admin.register показывает ещё одну особенность: декоратор выполняется один раз, в момент объявления функции или класса, а не при каждом вызове. Поэтому его удобно использовать для регистрации:
registry = []
def register(func):
registry.append(func.__name__)
return func # возвращаем функцию без изменений
@register
def export_csv(): ...
@register
def export_json(): ...
print(registry) # → ['export_csv', 'export_json']
Декоратор-класс
Декоратором может быть любой вызываемый объект, в том числе класс с методом __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 hello():
return "hi"
hello()
hello()
print(hello.calls) # → 2
Частые ошибки
wrapper не возвращает результат
def broken(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs) # нет return
return wrapper
@broken
def add(a, b):
return a + b
print(add(2, 3)) # → None
Пишите return func(*args, **kwargs), иначе декорированная функция всегда возвращает None.
Декоратор не возвращает wrapper
Если забыть return wrapper в конце декоратора, имя функции будет указывать на None, и вызов add(2, 3) упадёт с TypeError: 'NoneType' object is not callable.
Декоратор с параметрами без скобок
Если написать @repeat вместо @repeat(3), функция ping попадёт в параметр times, а имя ping будет указывать на внутренний decorator. Вызов ping("example.com") ничего не напечатает и молча вернёт объект функции, а ping() без аргументов упадёт с TypeError: repeat.<locals>.decorator() missing 1 required positional argument: 'func'. Декоратор с параметрами всегда вызывается со скобками.
Потерянное имя функции
Без functools.wraps у всех декорированных функций __name__ равно wrapper. Это ломает логи, документацию и фреймворки, которые различают функции по имени.
Обёртка принимает фиксированные аргументы
def wrapper(): работает только для функций без параметров. Используйте *args, **kwargs, чтобы декоратор подходил для любой функции, в том числе для методов класса: self просто придёт первым элементом args.
Частые вопросы
Как вызвать исходную функцию без декоратора?
Если обёртка создана с functools.wraps, исходная функция лежит в атрибуте __wrapped__. Это удобно в тестах, когда кэш, логирование или проверка прав мешают:
print(slow_sum.__wrapped__(2, 3)) # → 5, без замера времени
Без wraps атрибута __wrapped__ нет, и добраться до исходной функции снаружи нельзя.
Как написать декоратор для async-функции?
Обёртка тоже должна быть корутиной: объявите её через async def и дождитесь результата через await:
import asyncio
import functools
import time
def async_timer(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
start = time.perf_counter()
result = await func(*args, **kwargs)
print(f"{func.__name__}: {time.perf_counter() - start:.1f} с")
return result
return wrapper
@async_timer
async def load():
await asyncio.sleep(0.2)
return "ok"
print(asyncio.run(load())) # → load: 0.2 с, затем ok
Обычный синхронный wrapper вернёт корутину без await, и замер времени окажется бессмысленным.
Как сделать декоратор, который работает и со скобками, и без?
Первым параметром примите функцию со значением None, а настройки сделайте только именованными. Если декоратор вызван со скобками, функции ещё нет — верните его же с подставленными настройками:
import functools
def repeat(func=None, *, times=2):
if func is None:
return functools.partial(repeat, times=times)
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
@repeat # 2 раза
def a(): print("a")
@repeat(times=3) # 3 раза
def b(): print("b")
Можно ли декорировать класс?
Да, декоратор получает класс так же, как функцию, и возвращает класс — изменённый или новый. Так работают @dataclass и @admin.register(Article) в Django. Синтаксис тот же: @decorator над строкой class.
Как применить декоратор к функции из чужой библиотеки?
Вызовите декоратор вручную, без @: cached_get = functools.cache(some_lib.get). Исходная функция при этом не меняется, обёртка доступна под новым именем.
Итог
Декоратор — функция, которая принимает функцию и возвращает обёртку; @name над def — краткая форма func = name(func). Правильный шаблон: wrapper(*args, **kwargs), return func(*args, **kwargs), @functools.wraps(func); для параметров добавьте внешний уровень вложенности. Короткие функции, которые передают в другие функции, разобраны в статье о lambda в Python, а весь базовый синтаксис собран в шпаргалке по синтаксису Python.
- #Python
- #Декораторы
- #functools
- #Функции
- #Синтаксис