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

Ключевые тезисы

  • Декоратор — это паттерн программирования, позволяющий добавлять новый функционал к функции, не изменяя её исходный код.
  • Синтаксис @decorator — это синтаксический сахар для myfunc = decorator(myfunc).
  • Декоратор может быть функцией или классом, но чаще всего это функция.
  • Важные аспекты: работа с параметрами функции, декораторы с собственными параметрами, сохранение метаданных (@wraps) и поддержка асинхронных функций.

Основное содержание

🎯 Что такое декоратор?

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

Примеры использования:

  • Подсчёт времени исполнения функции.
  • Логирование параметров вызова.
  • Повторный вызов функции в случае ошибки (retry).
  • Ограничение частоты вызовов (rate limiting).

🛠️ Скелет декоратора (функция)

Базовый декоратор — это функция, которая принимает другую функцию (callable-объект) и возвращает новую функцию-обёртку.

from typing import Callable

def decorator(func: Callable) -> Callable:
    def wrapper(*args, **kwargs):
        # Код, выполняемый ДО вызова функции
        result = func(*args, **kwargs)  # Вызов исходной функции
        # Код, выполняемый ПОСЛЕ вызова функции
        return result
    return wrapper

📝 Декоратор с параметрами функции

Чтобы декоратор работал с любыми функциями (с аргументами или без), внутренняя обёртка wrapper должна принимать *args и **kwargs и передавать их в исходную функцию.

⚙️ Декоратор с собственными параметрами

Если декоратору нужно передать параметр (например, лимит вызовов), добавляется ещё один уровень вложенности — функция-фабрика.

def limit_calls(limit: int):
    def outer_wrapper(func: Callable) -> Callable:
        def inner_wrapper(*args, **kwargs):
            nonlocal limit  # Важно: объявление nonlocal для изменения переменной внешней функции
            if limit == 0:
                raise Exception("Нельзя вызвать функцию")
            limit -= 1
            return func(*args, **kwargs)
        return inner_wrapper
    return outer_wrapper

🔍 Сохранение метаданных функции

Применение декоратора заменяет метаданные исходной функции (имя, docstring) на метаданные обёртки. Чтобы сохранить их, используется декоратор wraps из модуля functools.

from functools import wraps

def decorator(func: Callable) -> Callable:
    @wraps(func)  # Сохраняет __name__, __doc__ и другие метаданные
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

⚡ Декоратор для асинхронных функций

Для оборачивания асинхронных функций (корутин) обёртка wrapper должна быть объявлена как async def, а вызов функции — через await.

from typing import Coroutine

def async_decorator(func: Coroutine) -> Callable:
    async def wrapper(*args, **kwargs):
        # Асинхронный код до вызова
        result = await func(*args, **kwargs)  # Асинхронный вызов
        # Асинхронный код после вызова
        return result
    return wrapper

💡 Полезные встроенные и модульные декораторы

  • @lru_cache (из functools): Кэширует результаты вызовов функции, что полезно для тяжёлых вычислений.
  • @contextmanager (из contextlib): Позволяет создать контекстный менеджер на основе функции-генератора (для использования с with).

Примеры/кейсы

  • Декоратор для логирования вызовов: Перед вызовом функции печатает её имя и переданные аргументы.
  • Декоратор для замера времени: Фиксирует время до и после выполнения функции, вычисляет и выводит разницу.

Выводы

  • Понимание принципа работы декораторов (myfunc = decorator(myfunc)) — ключ к решению любых задач на эту тему.
  • Важно помнить о сохранении метаданных (@wraps) и корректной обработке параметров (*args, **kwargs).
  • Умение писать декораторы для асинхронного кода и декораторы с параметрами отличает продвинутого разработчика.
  • Декораторы широко применяются в реальных проектах (веб-фреймворки, логирование, кэширование, валидация), поэтому их глубокое понимание критически важно.