
Декоратором в Python называют функцию, которая принимает другую функцию и возвращает новую, обычно обёртку вокруг исходной. Запись @имя над определением функции подменяет её результатом декоратора. Так можно расширять поведение функции: добавить логирование, замер времени или кэш без изменения исходного кода.
Все примеры ниже прогнаны в Python 3.14.3 как один файл: блоки идут подряд, и последний пользуется декораторами из предыдущих. Чтобы повторить, скопируйте их по порядку в один .py-файл и запустите. То, что печатает программа, скопировано из консоли под каждым блоком.
- Зачем нужны декораторы
- Что такое декоратор и что делает символ @
- Как написать свой декоратор для функции с аргументами
- functools.wraps: зачем он нужен
- Декораторы с параметрами
- Несколько декораторов: в каком порядке они работают
- Встроенные декораторы: @property, @staticmethod, @classmethod, lru_cache и cache
- Типичные ошибки с декораторами
- Где применяются декораторы на практике
- Частые вопросы о декораторах Python
- Чем декоратор отличается от обычной функции?
- С какой версии Python есть декораторы?
- Можно ли применить несколько декораторов к одной функции?
- Зачем нужен functools.wraps?
- Может ли декоратором быть класс?
- Когда срабатывает код декоратора?
Зачем нужны декораторы
Допустим, замер времени нужен в десяти функциях проекта. Без декоратора в каждую придётся вписать одни и те же три строчки: запомнить время до вызова, после вызова и напечатать разницу. Когда формат вывода понадобится изменить, правка пойдёт по десяти местам, и одно из них легко пропустить.
Декоратор позволяет держать такую логику в одном месте и подключать её одной строчкой над нужной функцией. В случае с замером времени это @timer.
Что такое декоратор и что делает символ @

Основа механизма такая: в Python функция ведёт себя как любой другой объект. Её можно положить в переменную, передать в качестве аргумента и вернуть из другой функции. Именно на этом держатся декораторы. Декоратор получает функцию func, определяет внутри себя функцию wrapper и возвращает её. Имя исходной функции после этого указывает уже на wrapper, а сама func остаётся доступна ему через замыкание. Закрепить синтаксис функций перед декораторами помогут задачи по Python для начинающих, а запускать примеры удобно в VS Code с настроенным Python.
Символ @ называют синтаксическим сахаром: две записи ниже делают одно и то же.
@log_calls
def say_hello(): ...
# то же самое без @
def say_hello(): ...
say_hello = log_calls(say_hello)
@ только сокращает запись. Разница одна: при записи через @ исходная функция ни на миг не попадает в имя say_hello. Вот простейший пример декоратора целиком:
def log_calls(func):
print(f"Декоратор получил функцию {func.__name__}")
def wrapper():
print(f"Вызов {func.__name__}")
func()
print(f"Готово: {func.__name__}")
return wrapper
@log_calls
def say_hello():
"""Печатает приветствие."""
print("Привет, мир!")
print("Определения закончились")
say_hello()
print(say_hello.__name__, say_hello.__doc__)
Вывод:
Декоратор получил функцию say_hello
Определения закончились
Вызов say_hello
Привет, мир!
Готово: say_hello
wrapper None
Обратите внимание: «Декоратор получил функцию» напечаталось раньше, чем «Определения закончились». Декоратор срабатывает, когда выполняется инструкция def; для функции на уровне модуля это происходит один раз, когда модуль выполняется или импортируется. При каждом вызове say_hello() работает уже wrapper: печатает сообщение до вызова, вызывает исходную функцию и печатает сообщение после.
В последней строчке функция уже называется wrapper, а вместо её docstring напечатано None.
Как написать свой декоратор для функции с аргументами
У декоратора log_calls функция wrapper не принимает аргументов и ничего не возвращает. Если применить его к функции с параметрами, вызов упадёт с TypeError, а результат исходной функции потеряется. Если хотим, чтобы декоратор подходил к любой функции, обёртка должна принимать *args и **kwargs и передавать их дальше без изменений. Рабочий шаблон такой: wrapper(*args, **kwargs) принимает любые позиционные и именованные аргументы, передаёт их в func, сохраняет result и возвращает его.

Декоратор timer измеряет время выполнения функции. Для замера нужен import time, для wraps импорт из functools.
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__} работала {end - start:.1f} с")
return result
return wrapper
@timer
def slow_sum(numbers, delay=0.2):
"""Складывает числа с задержкой."""
time.sleep(delay)
return sum(numbers)
total = slow_sum([4, 8, 15], delay=0.3)
print("Сумма:", total)
print(slow_sum.__name__, "|", slow_sum.__doc__)
print(slow_sum.__wrapped__([16, 23, 42], delay=0))
Результат:
slow_sum работала 0.3 с
Сумма: 27
slow_sum | Складывает числа с задержкой.
81
Модуль time здесь нужен дважды: time.perf_counter() берёт отметку времени перед вызовом и после, а time.sleep() имитирует медленную работу. Разница печатается с точностью до десятых секунды. Если запустите код у себя, время может немного отличаться. Список [4, 8, 15] попал в функцию через *args, аргумент delay=0.3 через **kwargs, а сумма 27 вернулась через return result. Забыть этот return легко, и тогда декорированная функция молча вернёт None вместо числа.
functools.wraps: зачем он нужен
Сравните строку wrapper None из первого примера и slow_sum | Складывает числа с задержкой. из второго. Без wraps функция назвалась wrapper и потеряла docstring. С @wraps(func) имя slow_sum и описание «Складывает числа с задержкой.» остались на месте.
wraps копирует обёртке метаданные исходной функции: __name__, __qualname__, __doc__, __module__, __annotations__, __type_params__, а ещё дополняет её __dict__ атрибутами оригинала. Кроме того, он добавляет атрибут __wrapped__ со ссылкой на исходную функцию. Через него в примере вызвана slow_sum в обход таймера, поэтому сообщения «работала» во второй раз нет. Сумма 81 напечаталась без паузы, потому что передан delay=0.
Без wraps декорированные функции в help() и в логах, которые берут __name__, выглядят одинаково: везде wrapper. Ставьте @wraps(func) в каждый свой декоратор, даже в учебный. Нужно всего лишь добавить импорт from functools import wraps и одну строчку над def wrapper.
Декораторы с параметрами
Иногда декоратору нужна настройка: сколько раз повторить вызов, какой уровень лога писать, в какой тег обернуть ответ. Тогда пишут ещё один уровень вложенности. Внешняя функция принимает параметры и возвращает настоящий декоратор, а тот уже принимает func.

Декоратор tag(name) из следующего блока оборачивает текст, который вернула функция, в HTML-тег:
from functools import wraps
def tag(name):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return f"<{name}>{result}</{name}>"
return wrapper
return decorator
@tag("b")
@tag("i")
def greet(name):
return f"Привет, {name}"
print(greet("Оля"))
def plain_greet(name):
return f"Привет, {name}"
manual = tag("b")(tag("i")(plain_greet))
print(manual("Ира"))
Вывод:
<b><i>Привет, Оля</i></b>
<b><i>Привет, Ира</i></b>
Запись @tag("b") выполняется в два шага. Сначала вызов tag("b") возвращает функцию decorator, у которой в замыкании сохранилось name = "b". Затем decorator применяется к функции, как обычный декоратор без параметров. Сам wrapper(*args, **kwargs) устроен так же, как в timer, и вызывает func с теми же аргументами. Отсюда три уровня вложенности: tag, decorator и wrapper.
Несколько декораторов: в каком порядке они работают
Если декораторов над одной функцией больше одного, они применяются снизу вверх. В примере выше @tag("i") стоит ближе к def, поэтому он оборачивает greet первым, а @tag("b") оборачивает уже результат. Ручная запись tag("b")(tag("i")(plain_greet)) даёт тот же вывод, так удобно проверить порядок, если сомневаетесь.
При вызове порядок обратный: первой срабатывает внешняя обёртка b, она вызывает внутреннюю i, та вызывает саму функцию. Поэтому <i> оказался внутри <b>.
Встроенные декораторы: @property, @staticmethod, @classmethod, lru_cache и cache
Готовые декораторы доступны без установки: одни встроены в язык и используются в классах, другие лежат в модуле functools стандартной библиотеки. В блоке ниже заодно есть декоратор класса register: он принимает класс, записывает его в словарь и возвращает тот же класс.

registry = {}
def register(cls):
registry[cls.__name__] = cls
return cls
@register
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError(f"{value} ниже абсолютного нуля")
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32
@staticmethod
def is_freezing(celsius):
return celsius <= 0
@classmethod
def from_fahrenheit(cls, fahrenheit):
return cls((fahrenheit - 32) * 5 / 9)
room = Temperature(22)
print(room.celsius, room.fahrenheit)
street = Temperature.from_fahrenheit(14)
print(street.celsius, Temperature.is_freezing(street.celsius))
try:
room.celsius = -300
except ValueError as error:
print("Ошибка:", error)
print(registry)
from functools import cache, lru_cache
@lru_cache(maxsize=None)
def fibonacci(number):
if number < 2:
return number
return fibonacci(number - 1) + fibonacci(number - 2)
print(fibonacci(80))
print(fibonacci.cache_info())
@cache
def count_words(sentence):
return len(sentence.split())
print(count_words("кэш хранит результат вызова"))
В консоли:
22 71.6
-10.0 True
Ошибка: -300 ниже абсолютного нуля
{'Temperature': <class '__main__.Temperature'>}
23416728348467685
CacheInfo(hits=78, misses=81, maxsize=None, currsize=81)
4
Метод celsius определён дважды с одним именем: сначала геттер, затем сеттер. @property делает его атрибутом: его читают без скобок, room.celsius. Пара @celsius.setter перехватывает присваивание, поэтому значение −300 не прошло проверку и вызвало ValueError. __init__ тоже пишет через сеттер, поэтому и через конструктор объект ниже абсолютного нуля не создать: Temperature(-300) вызывает тот же ValueError. fahrenheit задан только через @property без сеттера: его можно прочитать, но не присвоить.
@staticmethod убирает неявный первый аргумент. is_freezing не получает ни self, ни cls, это обычная функция, которая лежит в пространстве имён класса.
@classmethod передаёт первым аргументом класс, поэтому он удобен для создания альтернативных конструкторов. Таков from_fahrenheit: он пересчитывает 14 °F в −10 °C и вызывает cls(...). У наследника Temperature этот метод создаст объект наследника. Цепочку @classmethod поверх @property разрешили в Python 3.9, объявили устаревшей в 3.11 и убрали в 3.13.
@lru_cache(maxsize=None) запоминает результаты по аргументам. Рекурсивный fibonacci(80) без кэша считал бы одни и те же числа заново на каждой ветке, а с ним cache_info() возвращает статистику в виде именованного кортежа: 81 промах (каждое число от 0 до 80 посчитано один раз) и 78 попаданий. По умолчанию maxsize=128: кэш хранит 128 последних вызовов и вытесняет давние. @cache из Python 3.9 работает как lru_cache(maxsize=None), без ограничения размера. Сбросить сохранённые результаты можно вызовом cache_clear().
Использовать кэш стоит только для функций, которые для одних аргументов всегда возвращают одно и то же и ничего не меняют снаружи. Функция с print, записью в файл или запросом к серверу под кэшем выполнит эти действия только при первом вызове с данными аргументами.
Порядок декораторов здесь тоже важен. Если поставить @timer выше @lru_cache, таймер будет мерить и попадания в кэш; если ниже, замер будет только у настоящих вычислений.
Декоратор класса работает так же, как декоратор функции: Temperature = register(Temperature). Из стандартной библиотеки так устроен @dataclass (модуль dataclasses, с Python 3.7): он дописывает в класс сгенерированные __init__(), __repr__() и другие специальные методы.
Типичные ошибки с декораторами

Последний блок собирает четыре ошибки, которые легко сделать в собственных декораторах. Он пользуется timer, tag и count_words из предыдущих блоков.
def broken(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
# забыли return wrapper
try:
@broken
def ping():
return "pong"
ping()
except TypeError as error:
print("1)", error)
try:
@timer()
def shout(message):
return message.upper()
except TypeError as error:
print("2)", error)
@tag
def whisper(message):
return message.lower()
print("3)", whisper.__name__)
try:
count_words(["список", "не", "хешируется"])
except TypeError as error:
print("4)", error)
В консоли:
1) 'NoneType' object is not callable
2) timer() missing 1 required positional argument: 'func'
3) decorator
4) unhashable type: 'list'
| № | Что сделали | Что видно | Как исправить |
|---|---|---|---|
| 1 | Забыли return wrapper | TypeError: 'NoneType' object is not callable при вызове функции | Декоратор обязан вернуть wrapper: последней строкой return wrapper |
| 2 | Поставили скобки декоратору без параметров: @timer() | TypeError: timer() missing 1 required positional argument: 'func' прямо при определении | Писать @timer без скобок |
| 3 | Забыли скобки у декоратора с параметрами: @tag | Ошибки нет, но функция стала decorator, вызов вернёт ещё одну функцию вместо текста | Писать @tag("b") со скобками и параметром |
| 4 | Передали список в функцию под @cache или @lru_cache | TypeError: unhashable type: 'list' | Передавать кортеж или строку: аргументы кэшируемой функции должны быть хешируемыми |
Третья ошибка самая коварная: программа не падает там, где ошиблись. Функция whisper превратилась в decorator, и сломается уже код, который ждёт от неё текст. Проверка print(whisper.__name__) после декорирования ловит такую подмену сразу: с правильно применённым декоратором и wraps там было бы whisper.
Список не годится для кэша, потому что результаты хранятся в словаре, а ключ словаря должен быть хешируемым. Что такое хеш и как он считается, разобрано в статье про хеш-функции.
Где применяются декораторы на практике

Во фреймворках декораторы связывают функцию с внешней системой. В Flask @app.route("/") говорит, на какой URL отвечает функция-обработчик, а @app.errorhandler(404) назначает страницу ошибки. Сам фреймворк при этом вызывает вашу функцию, когда приходит запрос.
В своём коде декораторы хороши для сквозных задач, которые одинаковы для многих функций:
- логирование вызовов и аргументов для отладки;
- замер времени выполнения, как
timerвыше; - кэширование результатов;
- проверка, вошёл ли пользователь, до вызова обработчика;
- повтор запроса при сетевой ошибке.
Логику самой задачи в декоратор лучше не прятать. Если обёртка меняет аргументы или подменяет ответ функции, читатель кода видит одно, а получает другое. Допустим, tag из примера выше начал бы ещё и переводить имя в верхний регистр: тогда greet("Оля") вернул бы не то, что написано в теле greet, и причину пришлось бы искать в декораторе.
Частые вопросы о декораторах Python

Чем декоратор отличается от обычной функции?
Обычно декоратор и есть функция: она принимает функцию и возвращает новую функцию. По устройству она ничем не отличается от любой другой, особенной её делает только применение через @ перед определением функции: имя подменяется результатом вызова. В общем случае декоратором может быть любой вызываемый объект, в том числе класс.
С какой версии Python есть декораторы?
Декораторы функций и методов появились в Python 2.4, декораторы классов в Python 3.0. В 3.9 сняли ограничения на выражение после @.
Можно ли применить несколько декораторов к одной функции?
Да. Они применяются снизу вверх: ближний к функции оборачивает её первым. Порядок легко проверить ручной записью вроде tag("b")(tag("i")(func)).
Зачем нужен functools.wraps?
Чтобы обёртка сохранила имя, docstring и другие метаданные исходной функции и получила атрибут __wrapped__ со ссылкой на неё. Без wraps все декорированные функции называются wrapper.
Может ли декоратором быть класс?
Да. После @ годится любой вызываемый объект, который примет функцию единственным аргументом. Это может быть класс или экземпляр класса с методом __call__. Обратная сторона тоже работает: декоратор можно применить к самому классу, как register и @dataclass в примерах выше.
Когда срабатывает код декоратора?
Тело декоратора выполняется, когда выполняется инструкция def; для функции на уровне модуля это один раз, когда модуль выполняется или импортируется. Тело wrapper выполняется при каждом вызове. Если декоратор что-то печатает или регистрирует, это случится ещё до первого вызова функции, как в первом примере статьи.








