Декораторы в Python: синтаксис, параметры и примеры
Декораторы в Python позволяют добавить функции новое поведение, не меняя ее код: замер времени, логирование, кеширование, проверку прав. В статье показано, как устроен декоратор изнутри, как написать свой с параметрами и без, зачем нужен functools.wraps и какие декораторы уже есть в стандартной библиотеке.

Содержание статьи
Что такое декоратор
Декоратор - это вызываемый объект, который принимает функцию и возвращает другую функцию, обычно обертку вокруг исходной. Такое возможно, потому что функции в Python являются обычными объектами: их можно передать в аргументе, вернуть из другой функции и присвоить переменной.
Запись с символом @ над определением функции - это сокращение. Две формы ниже делают одно и то же:
@my_decorator
def say_hello():
print("Привет")
# то же самое без синтаксиса @
def say_hello():
print("Привет")
say_hello = my_decorator(say_hello)Декоратор применяется один раз, в момент определения функции, то есть при импорте модуля. Дальше при каждом вызове say_hello() выполняется уже обертка. Механизм опирается на замыкания: внутренняя функция запоминает переменную func из внешней.
Как написать свой декоратор
Классическая схема состоит из трех шагов: внешняя функция принимает func, внутри объявляется wrapper, внешняя функция возвращает wrapper без вызова.
def my_decorator(func):
def wrapper():
print("До вызова")
func()
print("После вызова")
return wrapper
@my_decorator
def say_hello():
print("Привет, мир!")
say_hello()
# До вызова
# Привет, мир!
# После вызоваУ такой обертки два недостатка. Она не принимает аргументы и теряет результат исходной функции. Универсальный вариант использует *args и **kwargs (подробнее в статье про аргументы функции) и обязательно возвращает результат:
def log_call(func):
def wrapper(*args, **kwargs):
print(f"Вызов {func.__name__}{args}")
result = func(*args, **kwargs)
print(f"Результат: {result}")
return result
return wrapper
@log_call
def add(a, b):
return a + b
add(3, 5)
# Вызов add(3, 5)
# Результат: 8Если забыть return result, декорированная функция будет всегда возвращать None. Это самая частая ошибка у тех, кто пишет первый декоратор.
Зачем нужен functools.wraps
После декорирования имя функции, ее docstring и аннотации подменяются атрибутами обертки. В примере выше add.__name__ вернет строку "wrapper", что мешает отладке, логированию и автодокументации. Декоратор functools.wraps копирует метаданные исходной функции в обертку:
import functools
def log_call(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@log_call
def add(a, b):
"""Складывает два числа."""
return a + b
print(add.__name__, add.__doc__)
# add Складывает два числа.Кроме того, wraps сохраняет ссылку на оригинал в атрибуте __wrapped__, поэтому исходную функцию можно вызвать в тестах напрямую. Ставьте @functools.wraps(func) в каждый декоратор, это правило хорошего тона.
Декоратор с параметрами
Когда декоратору нужны настройки, например число повторов, добавляется еще один уровень вложенности. Запись @repeat(3) сначала вызывает repeat(3), а уже возвращенный объект применяется к функции.
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def greet(name):
print(f"Hello, {name}")
greet("John") # строка выводится три разаПолучается фабрика декораторов: внешняя функция хранит параметры, средняя принимает функцию, внутренняя выполняет работу. Писать @repeat без скобок здесь нельзя, иначе в times попадет сама функция.
Несколько декораторов на одной функции
Декораторы можно ставить друг над другом. Применяются они снизу вверх: ближайший к def оборачивает функцию первым. А при вызове код оберток выполняется сверху вниз.
def bold(func):
return lambda: "<b>" + func() + "</b>"
def upper(func):
return lambda: func().upper()
@bold
@upper
def text():
return "hello"
print(text()) # <b>HELLO</b>Запись эквивалентна text = bold(upper(text)). Если поменять декораторы местами, получится <B>HELLO</B>: upper переведет в верхний регистр и сами теги. Например, кеширующий декоратор обычно ставят ближе к функции, а логирующий снаружи.
Декораторы для методов и классов
Обертка из примера с *args подходит и для методов: первым позиционным аргументом в нее придет self. Отдельно обрабатывать его не нужно, если не требуется доступ к экземпляру.
Декорировать можно и целый класс. Декоратор класса получает класс и возвращает его же (измененным) или новый объект. Простой пример - реестр, куда автоматически попадают классы плагинов:
PLUGINS = {}
def register(cls):
PLUGINS[cls.__name__] = cls
return cls
@register
class CsvExporter:
pass
print(PLUGINS) # {'CsvExporter': <class '__main__.CsvExporter'>}Встроенные декораторы
Многие задачи уже решены в стандартной библиотеке. Таблица показывает самые используемые декораторы.
| Декоратор | Где находится | Что делает |
|---|---|---|
@property | встроенный | Превращает метод в атрибут только для чтения, можно добавить setter |
@classmethod | встроенный | Метод получает класс cls вместо экземпляра |
@staticmethod | встроенный | Метод без self и cls |
@functools.cache | functools | Запоминает результаты вызовов без ограничения размера |
@functools.lru_cache(maxsize=128) | functools | Кеш с вытеснением давно не используемых значений |
@dataclass | dataclasses | Генерирует __init__, __repr__, __eq__ по аннотациям полей |
Подробные разборы есть в статьях про property и dataclasses.
Практические примеры
Ниже два декоратора, которые часто пишут в рабочих проектах. Оба сохраняют метаданные функции и возвращают ее результат.
Замер времени выполнения
Для измерения длительности используйте time.perf_counter(): это монотонный таймер с высокой точностью.
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:.4f} с")
return result
return wrapper
@timer
def compute(n):
return sum(range(n))
compute(1_000_000)Повтор при ошибке
Декоратор пробует вызвать функцию несколько раз и делает паузу между попытками. Пригодится для сетевых запросов, которые иногда падают по таймауту.
import functools
import time
def retry(attempts=3, delay=1.0, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for i in range(1, attempts + 1):
try:
return func(*args, **kwargs)
except exceptions:
if i == attempts:
raise
time.sleep(delay)
return wrapper
return decoratorПараметр exceptions ограничивает список ошибок, при которых есть смысл повторять вызов. Ловить все исключения подряд опасно: повторы скроют настоящую ошибку в коде.
Частые вопросы
Короткие ответы на вопросы, которые возникают при изучении декораторов.
Когда выполняется код декоратора?
Внешняя часть декоратора выполняется один раз при определении функции, то есть при импорте модуля. Код внутри wrapper выполняется при каждом вызове декорированной функции.
Можно ли снять декоратор с функции?
Если в декораторе использован functools.wraps, исходная функция доступна через атрибут __wrapped__. Без wraps добраться до нее штатно нельзя.
Чем декоратор отличается от замыкания?
Замыкание - это механизм, при котором вложенная функция помнит переменные внешней. Декоратор - прием, который использует замыкание, чтобы обернуть переданную функцию.
Может ли декоратор быть классом?
Да. Класс с методом __call__ работает как декоратор: в __init__ он получает функцию, а __call__ выполняется при каждом вызове. Такой вариант удобен, когда декоратору нужно хранить состояние, например счетчик вызовов.
Почему функция после декоратора возвращает None?
Обертка не вернула результат. Добавьте return func(*args, **kwargs) или сохраните результат в переменную и верните его в конце wrapper.