Декораторы Python
Как работают декораторы Python: написание собственных, сохранение метаданных с functools.wraps, стекирование и реальные примеры использования.
Декоратор — это функция, которая оборачивает другую функцию, чтобы расширить или изменить её поведение без изменения исходного кода. Декораторы — одна из самых мощных и идиоматичных возможностей Python: именно они лежат в основе @staticmethod, @classmethod, @property, @functools.lru_cache и множества паттернов популярных веб-фреймворков.
На этой странице рассматривается, как работают декораторы, как написать собственный с нуля, как передавать аргументы декораторам, как их стекировать и когда каждый паттерн наиболее уместен.
Как работают декораторы
Декоратор — это просто функция, которая принимает другую функцию в качестве аргумента и возвращает новую функцию. Python предоставляет синтаксис @ как сокращённый способ применить декоратор:
@shout
def greet(name):
return f"hello, {name}"Это в точности эквивалентно следующему:
def greet(name):
return f"hello, {name}"
greet = shout(greet)Строка @shout сообщает Python: после определения greet немедленно передать её в shout и привязать имя greet к тому, что вернёт shout. После этого каждый вызов greet(...) сначала проходит через логику shout.
Написание первого декоратора
Декоратор, как правило, определяет внутреннюю функцию-обёртку, которая вызывает оригинальную функцию и добавляет дополнительное поведение вокруг неё:
def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@shout
def greet(name):
return f"hello, {name}"
print(greet("world")) # HELLO, WORLD
print(greet("python")) # HELLO, PYTHONwrapper принимает *args и **kwargs, чтобы передавать любую комбинацию аргументов в func без изменений. Это делает декоратор совместимым с любой функцией вне зависимости от её сигнатуры — хорошая привычка с самого начала.
Почему обёртка должна возвращать внутреннюю функцию
shout заканчивается строкой return wrapper, а не return wrapper(). Это сделано намеренно: shout создаёт новый вызываемый объект, а не вызывает его. Если случайно написать return wrapper(), декоратор выполнится сразу в момент декорирования, и greet будет привязан к возвращаемому значению wrapper — строке — а не к самому вызываемому объекту.
Сохранение метаданных с помощью functools.wraps
Каждая функция Python несёт в себе метаданные: __name__, __doc__, __module__ и другие. Без дополнительных мер декоратор заменяет оригинальную функцию на wrapper, теряя все эти данные:
def shout(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@shout
def greet(name):
"""Say hello to name."""
return f"hello, {name}"
print(greet.__name__) # wrapper — wrong
print(greet.__doc__) # None — lostИсправить это можно, применив @functools.wraps(func) к обёртке. Это копирует метаданные оригинальной функции на wrapper:
import functools
def shout(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@shout
def greet(name):
"""Say hello to name."""
return f"hello, {name}"
print(greet("world")) # HELLO, WORLD
print(greet.__name__) # greet
print(greet.__doc__) # Say hello to name.Всегда используйте @functools.wraps в любом декораторе, который вы пишете. Без него инструменты отладки, генераторы документации и тестовые фреймворки видят неправильное имя функции. Исключение — когда вы намеренно хотите скрыть исходную идентичность функции.
Практические примеры декораторов
Логгер
Записывать каждый вызов функции с её аргументами и возвращаемым значением:
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args} kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(3, 5)
# Calling add with args=(3, 5) kwargs={}
# add returned 8Таймер
Измерять время выполнения функции:
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__} took {elapsed:.6f}s")
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
total = slow_sum(1_000_000)
print(total) # slow_sum took 0.01xxs then 499999500000time.perf_counter() — правильный выбор здесь, поскольку он обеспечивает наивысшее доступное разрешение для измерений коротких интервалов времени.
Мемоизация (кэш)
Кэшировать возвращаемое значение для каждого уникального набора аргументов, чтобы функция никогда не вычислялась дважды для одних и тех же входных данных:
import functools
def memoize(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10)) # 55
print(fibonacci(30)) # 832040В продакшн-коде предпочтительнее использовать встроенные @functools.lru_cache или @functools.cache (Python 3.9+), которые обрабатывают граничные случаи, потокобезопасность и ограничения размера кэша. Рукописная версия выше полезна для понимания паттерна.
Контроль доступа
Защищать функцию так, чтобы она выполнялась только при выполнении условия:
import functools
def require_auth(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get("is_authenticated"):
raise PermissionError("Authentication required.")
return func(user, *args, **kwargs)
return wrapper
@require_auth
def get_dashboard(user):
return f"Welcome, {user['name']}!"
guest = {"name": "Guest", "is_authenticated": False}
admin = {"name": "Admin", "is_authenticated": True}
try:
print(get_dashboard(guest))
except PermissionError as e:
print(e) # Authentication required.
print(get_dashboard(admin)) # Welcome, Admin!Декораторы с аргументами
Иногда нужно настроить декоратор в момент декорирования — например, повторить функцию переменное число раз. Обычные декораторы не могут принимать дополнительные аргументы напрямую, потому что Python передаёт функцию, а не аргументы. Решение — фабрика декораторов: функция, которая принимает конфигурацию и возвращает декоратор:
import functools
def repeat(n):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say(message):
print(message)
say("hello")
# hello
# hello
# helloЧитая снаружи внутрь: @repeat(3) сначала вызывает repeat(3), что возвращает decorator. Затем Python применяет decorator к say, что возвращает wrapper. Таким образом, say в итоге указывает на wrapper — тот же паттерн, что и раньше, с дополнительным уровнем только для того, чтобы передать n в область видимости.
Вложенность поначалу может казаться пугающей. Мысленная подсказка: самая внешняя функция хранит конфигурацию, средняя — декорируемую функцию, а самая внутренняя — перехватываемый вызов.
Стекирование нескольких декораторов
Вы можете применить несколько декораторов к одной функции, стекируя строки @. Python применяет их снизу вверх — декоратор, ближайший к def, применяется первым:
import functools
def bold(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return "<b>" + func(*args, **kwargs) + "</b>"
return wrapper
def italic(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return "<i>" + func(*args, **kwargs) + "</i>"
return wrapper
@bold
@italic
def greet(name):
return f"Hello, {name}"
print(greet("Alice")) # <b><i>Hello, Alice</i></b>Это эквивалентно greet = bold(italic(greet)). italic оборачивает greet первым, затем bold оборачивает результат. Вывод показывает, что italic выполняется ближе к исходной строке, а bold оборачивает снаружи.
Декораторы на основе классов
Класс тоже может быть декоратором — любой объект с методом __call__ является вызываемым. Декораторы на основе классов удобны, когда самому декоратору нужно хранить состояние между вызовами:
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"Call #{self.count} to {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello()
say_hello()
print(say_hello.count) # 2functools.update_wrapper(self, func) выполняет ту же работу, что и @functools.wraps — копирует метаданные оригинальной функции на экземпляр. После декорирования say_hello является экземпляром CountCalls, поэтому say_hello.count — это обычный доступ к атрибуту.
Когда выбирать класс вместо функции-декоратора:
- Нужно постоянное состояние (
count,cache, флаги). - Декоратор имеет несколько методов или вспомогательную логику.
- Нужно, чтобы декорированный объект был интроспектируемым как определённый тип.
Подводные камни декораторов
Забыть вызвать декорируемую функцию
Распространённая ошибка на начальном этапе — возвращать обёртку, забыв вызвать func внутри неё:
def broken(func):
def wrapper(*args, **kwargs):
print("before")
# forgot to call func!
return wrapperДекорированная функция молча возвращает None каждый раз. Всегда убеждайтесь, что wrapper вызывает func(*args, **kwargs) и возвращает его результат.
Декорирование не на том уровне
При использовании параметризованных декораторов часто допускают ошибку, забывая о внешнем вызове:
# Wrong — 'repeat' receives the function, not a count
@repeat # should be @repeat(3)
def say(msg):
print(msg)Это передаёт say в repeat там, где ожидается n, что вызывает TypeError при вызове say.
Порядок декораторов имеет значение
При стекировании декораторов порядок меняет поведение. @timer, а затем @log_calls на одной функции будет измерять время уже залогированной версии, тогда как обратный порядок будет логировать уже замеренную версию. Подумайте, что именно каждый уровень должен видеть.
Связь с замыканиями
Функция wrapper декоратора является замыканием — она захватывает func из охватывающей области видимости и сохраняет её в живых даже после того, как внешняя функция-декоратор вернула результат. Понимание замыканий делает устройство декораторов очевидным: объект-ячейка, хранящий func, — это именно то, что позволяет wrapper вызывать оригинальную функцию спустя долгое время после завершения shout(greet).
Синтаксис *args и **kwargs, используемый внутри обёрток, описан в отдельной главе. О лямбда-выражениях, хорошо сочетающихся с декораторами в паттернах высшего порядка, см. главу о лямбда.
Краткий справочник
| Паттерн | Когда использовать |
|---|---|
Базовая wrapper | Добавить поведение до/после функции |
@functools.wraps | Всегда — сохраняет __name__, __doc__ |
| Фабрика декораторов (3 уровня) | Нужна настройка декоратора |
| Стекированные декораторы | Объединить несколько независимых поведений |
| Декоратор на основе класса | Нужно постоянное состояние между вызовами |
@functools.lru_cache | Мемоизация чистых функций (встроенный, готов к продакшну) |