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

URL: https://skillaria.top/ru/guides/python/dekoratory-python

Декоратор в 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': }
```

Обратная ситуация — класс в роли декоратора. Подойдёт любой объект с методом `__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.

## FAQ

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

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

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

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

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

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

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

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


---
Source: Skillaria.Top (https://skillaria.top)
