Декораторы в Python: раздвигая рамки метапрограммирования

python
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 в стандартной библиотеке, вы наблюдаете применение мощной техники метапрограммирования.

Фундаментальная механика проста: декоратор принимает функцию и возвращает другую функцию, обычно добавляя дополнительное поведение. Но опасность поверхностного понимания в том, что многие разработчики используют декораторы как черные ящики, не осознавая компромиссов.

python
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

Такой простой декоратор демонстрирует ключевой паттерн, но настоящие проблемы возникают при масштабировании:

  1. Порядок применения имеет значение: порядок декораторов влияет на их поведение
  2. Отладка усложняется: stack trace показывает обернутую функцию
  3. Неочевидное поведение при оборачивании классов

Конструкции фабричного уровня

Настоящая мощь проявляется в декораторах, возвращающих параметризированные обертки. Рассмотрим паттерн декоратора фабрики:

python
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

Этот подход устраняет дублирование проверок в бизнес-логике, сохраняя сигнатуры функций чистыми.

Классы в роли декораторов

Декоратор не ограничивается функциями. Реализация через классы раскрывает дополнительные возможности управления состоянием:

python
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 применение декораторов требует особого подхода из-за природы корутин:

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 и логирование.

python
from functools import wraps

def proper_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        ...
    return wrapper

Столкновение подписей: В Python 3.5+ используйте inspect.signature для прозрачной обработки параметров:

python
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

Цепочная композиция: При множественных декораторах важно понимать их порядок:

python
@decorator1
@decorator2
def your_function():
    ...

Эквивалентно decorator1(decorator2(your_function)). Правило: декораторы применяются от ближайшего к функции к самому дальнему.

Реальные применения: неочевидные кейсы

Контекстные менеджеры как декораторы:

python
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):
    # Модификация данных в БД
    ...

Динамическая конфигурация:

python
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):
    ...

В этом примере декоратор полностью заменяет функцию пустой заглушкой при отключенном флаге, исключая вызов новой реализации из кода.

Стоимость и альтернативы

Декораторы добавляют накладные расходы на этапе импорта и выполнения. Для высоконагруженных участков кода оцените:

  1. Импакт времени загрузки модуля
  2. Глубину стека вызовов
  3. Альтернативные подходы:
    • Классы с явными методами
    • Композиция объектов
    • Контекстные менеджеры для временного поведения

Иногда явная реализация лучше:

python
# Декоратор
@measure_performance
def complex_operation():
    ...

# Альтернатива
def complex_operation():
    with PerformanceTracker('complex_operation'):
        ...

Выбирайте декораторы когда:

  • Поведение постоянное для функции
  • Требуется глобальное применение политики
  • Интегрируетесь с фреймворками

Предпочитайте явные конструкции когда:

  • Поведение динамически меняется
  • Работаете с асинхронными системами высокого уровня
  • Требуется лучшая видимость контроля

Эволюция инструментария

Python 3.10 представил интригующий синтаксис родительских декораторов через @, но настоящий прорыв происходит в системах типизации. Рассмотрим аннотации:

python
def type_aware(func: Callable) -> Callable:
    ...

Декораторы могут взаимодействовать с типами через протоколы и дженерики. В сочетании с инструментами вроде mypy это открывает возможности для статической валидации поведения.

При работе со сложными архитектурами стоит учитывать и альпака-декораторы — паттерн гибридов декораторов и контекстных менеджеров, популярный в современной экосистеме Python.

Декораторы — ножницы гиганта в мире метапрограммирования Python. Их верное применение требует понимания подводных камней и компромиссов, но врезультате вы получаетt инструмент формирования архитектуры приложений на уровне языка.