Перейти к содержанию
Innovialabs

Декораторы в Python: как работают и как написать свой

Практика3 мин

Коротко

Декоратор — это функция, которая принимает функцию и возвращает новую, добавляя поведение до или после вызова; синтаксис @decorator лишь сокращает запись func = decorator(func).

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

Декоратор — это функция, которая принимает другую функцию и возвращает новую. Запись @timer над функцией — просто короткая форма для func = timer(func). Декораторы позволяют вынести повторяющуюся логику — логирование, замер времени, повторы, проверку прав — из бизнес-кода в одно место.

Работают они благодаря двум свойствам Python: функции — это обычные объекты, и вложенная функция «помнит» переменные внешней (это и есть замыкание).

Шаг 1: замыкание

def make_greeter(greeting):
    def greet(name):
        return f"{greeting}, {name}!"
    return greet

hello = make_greeter("Привет")
print(hello("Анна"))  # Привет, Анна!

Внутренняя greet продолжает видеть greeting после того, как make_greeter завершилась. Декоратор устроен так же, только вместо строки во внешнюю функцию передаётся функция.

Шаг 2: первый декоратор и functools.wraps

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:.3f} c")
        return result
    return wrapper

@timer
def build_report(n):
    return sum(range(n))

Ключевые детали:

  • *args, **kwargs — обёртка принимает любые аргументы и передаёт их дальше.
  • return result — без него декорированная функция будет возвращать None.
  • functools.wraps копирует __name__, __doc__ и другие атрибуты оригинала. Без него все функции станут называться wrapper, что ломает логи, отладку и документацию.

Шаг 3: декоратор с аргументами

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

def retry(times=3, delay=1.0, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions:
                    if attempt == times:
                        raise
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(times=5, delay=0.5, exceptions=(ConnectionError,))
def fetch_rates():
    ...

Обратите внимание: @retry() со скобками и @retry без них — разные вещи. Во втором случае функция попадёт в параметр times, и код сломается.

Практические примеры

Кэширование. Для чистых функций не пишите свой кэш — в стандартной библиотеке есть functools.lru_cache и functools.cache:

@functools.lru_cache(maxsize=256)
def get_tariff(region_id):
    ...

Аргументы должны быть хешируемыми, а кэш живёт в памяти процесса.

Проверка прав. Декоратор удобно ставить на обработчики, где первым аргументом приходит пользователь:

def require_role(role):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(user, *args, **kwargs):
            if role not in user.roles:
                raise PermissionError(f"Нужна роль {role}")
            return func(user, *args, **kwargs)
        return wrapper
    return decorator

@require_role("admin")
def delete_order(user, order_id):
    ...

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

Это два разных понятия:

ЧтоКак выглядитКогда полезно
Класс как декораторкласс с __init__(self, func) и __call__нужно хранить состояние, например счётчик вызовов
Декоратор классафункция принимает класс и возвращает классрегистрация, добавление методов; так работает @dataclass
class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.calls = 0

    def __call__(self, *args, **kwargs):
        self.calls += 1
        return self.func(*args, **kwargs)

Частые ошибки

  • Забыть functools.wraps — теряются имя и docstring.
  • Забыть return в обёртке — функция молча возвращает None.
  • Путать @deco и @deco() у декораторов с параметрами.
  • Не учитывать порядок: декораторы применяются снизу вверх, ближайший к def — первым.
  • Оборачивать async def обычной обёрткой: для корутин обёртка тоже должна быть async и делать await func(...).
  • Прятать в декоратор слишком много логики — если поведение неочевидно из названия, читателю кода будет тяжело.

Частые вопросы

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

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.