def retry(max_attempts=3, delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
attempt = 0
while attempt < max_attempts:
try:
return func(*args, **kwargs)
except Exception as e:
attempt += 1
if attempt == max_attempts:
raise
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=5, delay=0.5)
def fetch_api_data(url):
# Реальная логика запроса
...
За кулисами обычной аннотации
Декораторы Python — это не просто элегантный синтаксис для обертывания функций. Их истинная сила раскрывается в архитектурных решениях и реализации cross-cutting concerns. Когда вы видите @app.route()
во Flask, @login_required
в Django или @lru_cache
в стандартной библиотеке, вы наблюдаете применение мощной техники метапрограммирования.
Фундаментальная механика проста: декоратор принимает функцию и возвращает другую функцию, обычно добавляя дополнительное поведение. Но опасность поверхностного понимания в том, что многие разработчики используют декораторы как черные ящики, не осознавая компромиссов.
def log_execution(func):
@wraps(func)
def wrapped(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned")
return result
return wrapped
@log_execution
def calculate(a, b):
return a * b
Такой простой декоратор демонстрирует ключевой паттерн, но настоящие проблемы возникают при масштабировании:
- Порядок применения имеет значение: порядок декораторов влияет на их поведение
- Отладка усложняется: stack trace показывает обернутую функцию
- Неочевидное поведение при оборачивании классов
Конструкции фабричного уровня
Настоящая мощь проявляется в декораторах, возвращающих параметризированные обертки. Рассмотрим паттерн декоратора фабрики:
def validate_types(expected_input, expected_output):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Валидация входных аргументов
for arg, (arg_val, expected_type) in zip(args, expected_input):
if not isinstance(arg_val, expected_type):
raise TypeError(f"Argument {arg} should be {expected_type}")
result = func(*args, **kwargs)
# Валидация результата
if not isinstance(result, expected_output):
raise TypeError( f"Return value should be {expected_output}")
return result
return wrapper
return decorator
@validate_types([(0, int), (1, int)], int)
def add(a, b):
return a + b
Этот подход устраняет дублирование проверок в бизнес-логике, сохраняя сигнатуры функций чистыми.
Классы в роли декораторов
Декоратор не ограничивается функциями. Реализация через классы раскрывает дополнительные возможности управления состоянием:
class RateLimiter:
def __init__(self, calls_per_minute):
self.calls_per_minute = calls_per_minute
self.last_reset = time.time()
self.call_count = 0
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
self._update_counter()
if self.call_count >= self.calls_per_minute:
raise RateLimitExceeded("Too many calls")
self.call_count += 1
return func(*args, **kwargs)
return wrapper
def _update_counter(self):
now = time.time()
if now - self.last_reset > 60:
self.last_reset = now
self.call_count = 0
@RateLimiter(calls_per_minute=30)
def api_request():
# Логика вызова API
...
Такая реализация инкапсулирует состояние между вызовами, что невозможно при использовании чистых функций.
Асинхронные вызовы: новая территория
В асинхронном Python применение декораторов требует особого подхода из-за природы корутин:
def async_retry(max_retries=3, delay=1):
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
attempts = 0
while attempts < max_retries:
try:
return await func(*args, **kwargs)
except TransientError:
attempts += 1
if attempts == max_retries:
raise
await asyncio.sleep(delay)
return wrapper
return decorator
@async_retry(max_retries=4)
async def fetch_user_data(user_id):
# Асинхронная операция
...
Ключевые особенности: использование async def
во внутренней обертке и await
при вызове целевой функции. Ошибка здесь — обработка исключений без учета специфики асинхронных ошибок или блокировка event loop.
Практические ловушки и их решения
Проблема метаданных: Не стоит недооценивать @functools.wraps
. Без него теряется имя функции, документация и другие атрибуты, что ломает introspection и логирование.
from functools import wraps
def proper_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
...
return wrapper
Столкновение подписей: В Python 3.5+ используйте inspect.signature
для прозрачной обработки параметров:
from inspect import signature, Parameter
def preserve_signature(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
wrapper.signature = signature(func)
return wrapper
Цепочная композиция: При множественных декораторах важно понимать их порядок:
@decorator1
@decorator2
def your_function():
...
Эквивалентно decorator1(decorator2(your_function))
. Правило: декораторы применяются от ближайшего к функции к самому дальнему.
Реальные применения: неочевидные кейсы
Контекстные менеджеры как декораторы:
class Transactional:
def __init__(self, session):
self.session = session
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
result = func(*args, **kwargs)
self.session.commit()
return result
except Exception:
self.session.rollback()
raise
return wrapper
@Transactional(db_session)
def update_inventory(item_id, quantity):
# Модификация данных в БД
...
Динамическая конфигурация:
def feature_flag(flag_name):
def decorator(func):
if not features.is_enabled(flag_name):
return no_op_wrapper # Возвращаем пустышку
return func
return decorator
@feature_flag('new-checkout-flow')
def process_order(order):
...
В этом примере декоратор полностью заменяет функцию пустой заглушкой при отключенном флаге, исключая вызов новой реализации из кода.
Стоимость и альтернативы
Декораторы добавляют накладные расходы на этапе импорта и выполнения. Для высоконагруженных участков кода оцените:
- Импакт времени загрузки модуля
- Глубину стека вызовов
- Альтернативные подходы:
- Классы с явными методами
- Композиция объектов
- Контекстные менеджеры для временного поведения
Иногда явная реализация лучше:
# Декоратор
@measure_performance
def complex_operation():
...
# Альтернатива
def complex_operation():
with PerformanceTracker('complex_operation'):
...
Выбирайте декораторы когда:
- Поведение постоянное для функции
- Требуется глобальное применение политики
- Интегрируетесь с фреймворками
Предпочитайте явные конструкции когда:
- Поведение динамически меняется
- Работаете с асинхронными системами высокого уровня
- Требуется лучшая видимость контроля
Эволюция инструментария
Python 3.10 представил интригующий синтаксис родительских декораторов через @
, но настоящий прорыв происходит в системах типизации. Рассмотрим аннотации:
def type_aware(func: Callable) -> Callable:
...
Декораторы могут взаимодействовать с типами через протоколы и дженерики. В сочетании с инструментами вроде mypy это открывает возможности для статической валидации поведения.
При работе со сложными архитектурами стоит учитывать и альпака-декораторы — паттерн гибридов декораторов и контекстных менеджеров, популярный в современной экосистеме Python.
Декораторы — ножницы гиганта в мире метапрограммирования Python. Их верное применение требует понимания подводных камней и компромиссов, но врезультате вы получаетt инструмент формирования архитектуры приложений на уровне языка.