Основы

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

Обновлено: · Skillaria.Top

Декоратор в Python — это функция, которая принимает другую функцию и возвращает новую, обычно обёртку, добавляющую поведение до или после вызова: логирование, замер времени, кэширование, проверку прав. Запись @decorator над def — сокращение для func = decorator(func); чтобы обёртка сохранила имя и docstring исходной функции, используют functools.wraps.

Декоратор в Python — это функция, которая принимает функцию и возвращает другую функцию, как правило обёртку вокруг исходной. Так к функции добавляют поведение — логирование, замер времени, кэш, повторные попытки — не меняя её код. Декораторы в Python пишутся строкой @имя над объявлением функции и встречаются повсюду: во Flask и FastAPI, в pytest, в стандартной библиотеке.

Что такое декоратор простыми словами

Представьте подарок и обёрточную бумагу. Подарок (функция) остаётся тем же, но снаружи появляется слой, который срабатывает первым. Декоратор и есть такая упаковка: при вызове сначала выполняется код обёртки, она вызывает исходную функцию и может что-то сделать с результатом.

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

Как работает декоратор: @ — это синтаксический сахар

Возьмём простейший декоратор, который печатает сообщение до и после вызова:

def log_calls(func):
    def wrapper():
        print(f"Вызываю {func.__name__}")
        result = func()
        print(f"{func.__name__} завершилась")
        return result
    return wrapper

@log_calls
def hello():
    print("Привет!")

hello()
Вызываю hello
Привет!
hello завершилась

Запись @log_calls над def полностью эквивалентна строке после объявления:

def hello():
    print("Привет!")

hello = log_calls(hello)

Имя hello теперь указывает на wrapper, а исходная функция живёт внутри замыкания — wrapper помнит переменную func из внешней функции. Важная деталь: декоратор применяется один раз, в момент выполнения def, то есть при импорте модуля, а обёртка срабатывает при каждом вызове.

Замыкания и декораторы

Замыкание — вложенная функция, которая запоминает переменные внешней функции после её завершения. Без замыканий декораторов бы не было: именно так wrapper получает доступ к func. В замыкании можно хранить и состояние, например счётчик вызовов:

def count_calls(func):
    calls = 0
    def wrapper(*args, **kwargs):
        nonlocal calls
        calls += 1
        print(f"{func.__name__}: вызов №{calls}")
        return func(*args, **kwargs)
    return wrapper

Как написать декоратор в Python: универсальный шаблон

У первого примера есть два недостатка: обёртка не принимает аргументы и «съедает» имя исходной функции. Рабочий шаблон, с которого стоит начинать любой декоратор, выглядит так:

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # код до вызова
        result = func(*args, **kwargs)
        # код после вызова
        return result
    return wrapper

Три правила шаблона:

  1. *args, **kwargs — обёртка принимает любые аргументы и передаёт их дальше без изменений.
  2. return result — не забыть вернуть результат, иначе декорированная функция всегда будет отдавать None.
  3. @functools.wraps(func) — перенести метаданные исходной функции на обёртку.

Пример декоратора — замер времени выполнения:

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__} заняла {elapsed:.3f} с")
        return result
    return wrapper

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

slow_sum(10_000_000)   # slow_sum заняла 0.142 с

Зачем нужен functools.wraps

Без wraps у декорированной функции подменяются имя, docstring и другие атрибуты — снаружи виден wrapper:

def plain(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@plain
def add(a, b):
    """Складывает два числа."""
    return a + b

print(add.__name__, add.__doc__)   # wrapper None

С @functools.wraps(func) обёртка получает __name__, __doc__, __module__, __qualname__, __annotations__ исходной функции, а в атрибуте __wrapped__ хранится ссылка на оригинал. Это важно для отладки и трейсбеков, для help(), для фреймворков, которые регистрируют функции по имени, и для inspect.signature, который показывает настоящую сигнатуру.

Декоратор с параметрами (с аргументами)

Иногда декоратору нужны настройки: @retry(times=3), @app.route("/"). Для этого добавляют ещё один уровень вложенности. Внешняя функция принимает параметры и возвращает настоящий декоратор — такую конструкцию называют фабрикой декораторов.

import functools

def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def ping():
    print("ping")

ping()   # ping, ping, ping — три строки

Порядок вызовов: repeat(times=3) возвращает decorator, тот применяется к ping и возвращает wrapper. Иначе говоря, @repeat(times=3) — это ping = repeat(times=3)(ping).

Практичный пример — повтор при ошибке:

import functools
import time

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_rate():
    ...

Несколько декораторов на одной функции

Декораторы можно ставить друг над другом. Применяются они снизу вверх: ближний к def — первым.

@timer
@log_calls
def report():
    ...

# то же самое:
report = timer(log_calls(report))

При вызове внешний слой (timer) срабатывает первым, поэтому он замерит и время работы log_calls. Порядок имеет значение, например, если один декоратор проверяет права доступа, а другой кэширует результат.

Стандартные и встроенные декораторы Python

Многие полезные декораторы уже есть в языке и стандартной библиотеке:

ДекораторЧто делает
@propertyпревращает метод в атрибут только для чтения; с @x.setter — в управляемый атрибут
@staticmethodметод класса без self, обычная функция в пространстве имён класса
@classmethodметод получает класс cls вместо экземпляра — так пишут альтернативные конструкторы
@functools.cache, @functools.lru_cacheкэшируют результаты по аргументам
@functools.wrapsкопирует метаданные функции на обёртку
@dataclasses.dataclassгенерирует __init__, __repr__, __eq__ для класса
@contextlib.contextmanagerделает из генератора менеджер контекста для with

Кэширование: functools.cache и lru_cache

Кэширующий декоратор запоминает результат для каждого набора аргументов и при повторном вызове возвращает его без вычисления. Классический пример — рекурсивные числа Фибоначчи, которые без кэша считаются экспоненциально долго:

import functools

@functools.cache
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(100))   # 354224848179261915075

functools.cache (появился в Python 3.9) хранит все результаты без ограничений. @functools.lru_cache(maxsize=128) держит только последние записи и вытесняет старые — это безопаснее для долго работающих сервисов. Статистику смотрят через fib.cache_info(), сбрасывают — через fib.cache_clear(). Аргументы таких функций должны быть хешируемыми: список в качестве аргумента вызовет TypeError: unhashable type: 'list'. И кэшировать стоит только чистые функции — те, что при одинаковых аргументах всегда возвращают одно и то же и не имеют побочных эффектов.

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

@property позволяет обращаться к методу как к атрибуту, без скобок, и при необходимости контролировать присваивание:

class Account:
    def __init__(self, balance):
        self._balance = balance

    @property
    def balance(self):
        return self._balance

    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("Баланс не может быть отрицательным")
        self._balance = value

acc = Account(100)
print(acc.balance)   # 100
acc.balance = -5     # ValueError

staticmethod и classmethod

class Money:
    def __init__(self, kopecks):
        self.kopecks = kopecks

    @classmethod
    def from_rubles(cls, rubles):
        return cls(round(rubles * 100))

    @staticmethod
    def is_valid(value):
        return value >= 0

m = Money.from_rubles(12.5)
print(m.kopecks, Money.is_valid(-1))   # 1250 False

Именно @classmethod позволяет методу работать с самим классом, а не с его экземплярами: from_rubles корректно создаст и объект подкласса.

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

Декоратор можно применить к классу: он получает класс и возвращает класс (тот же или изменённый). Так устроен @dataclass. Простой пример — реестр классов:

registry = {}

def register(cls):
    registry[cls.__name__] = cls
    return cls

@register
class CsvExporter:
    pass

print(registry)   # {'CsvExporter': <class '__main__.CsvExporter'>}

Обратная ситуация — класс в роли декоратора. Подойдёт любой объект с методом __call__; состояние удобно хранить в атрибутах экземпляра:

import functools

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)

@CountCalls
def tick():
    pass

tick(); tick()
print(tick.calls)   # 2

Асинхронный декоратор

Для async def-функций обёртка тоже должна быть корутиной и ждать результат через await, иначе вернётся невыполненный объект корутины:

import functools

def log_async(func):
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        print(f"Старт {func.__name__}")
        result = await func(*args, **kwargs)
        print(f"Готово {func.__name__}")
        return result
    return wrapper

Зачем нужны декораторы в Python

  • Сквозная логика — логирование, метрики, замер времени без копирования кода в каждую функцию.
  • Кэширование — @functools.cache для дорогих чистых вычислений.
  • Проверки — права доступа, валидация аргументов, авторизация в веб-обработчиках.
  • Регистрация — маршруты во Flask и FastAPI (@app.get("/")), фикстуры pytest.
  • Надёжность — повторные попытки, таймауты, обработка ошибок.

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

Типичные ошибки

  • Обёртка не возвращает результат — функция после декорирования отдаёт None.
  • Нет *args, **kwargs — TypeError при вызове с аргументами.
  • Забыли wraps — неверные __name__ и docstring, путаница в логах.
  • Скобки у фабрики — @repeat вместо @repeat(3): функция попадёт в параметр times.
  • Тяжёлый код в теле декоратора — он выполняется при импорте модуля, а не при вызове.

Итог

Декораторы в Python — это функции высшего порядка: принимают функцию, возвращают обёртку, а @ — лишь короткая запись присваивания. Начинайте с шаблона с *args, **kwargs, return result и functools.wraps, для настроек добавляйте уровень фабрики. Функции, замыкания и аннотации, на которых держатся декораторы, разобраны на практике в курсе по Python.

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

Можно ли снять декоратор с функции?

Если декоратор использует functools.wraps, оригинал доступен через атрибут __wrapped__: func.__wrapped__(...) вызовет функцию без обёртки. Иначе остаётся только убрать строку с @ из кода.

Когда выполняется код декоратора?

Тело самого декоратора выполняется один раз, при объявлении функции, обычно во время импорта модуля. Код внутри wrapper выполняется при каждом вызове декорированной функции.

Можно ли декорировать методы класса?

Да, обычный декоратор с *args, **kwargs работает и с методами: self просто окажется первым элементом args.

Чем декоратор отличается от наследования?

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

Разберите тему на видео

Эта тема подробно разобрана с практикой в курсе «Python 3.15: от новичка до профи».

Перейти к курсу