Декораторы в Python: обёртка вокруг функции
Собираем декоратор руками: функция-обёртка, знак @, пропуск любых аргументов насквозь и functools.wraps, чтобы функция не теряла имя.
Редакция Питоники
Вы уже умеете передавать функцию в функцию и возвращать её из функции — это были функции как значения. Декоратор — это функция, которая принимает другую функцию и возвращает новую: поведение добавляется без правки исходного кода. Логирование, замер времени, проверка доступа — всё это дописывается обёрткой снаружи, а функция внутри остаётся нетронутой. Само слово страшнее механизма: разберём декоратор по косточкам, без знака @.
Обёртка руками: без волшебного знака
Задача: сделать из тихой функции громкую — перевести её ответ в верхний регистр и добавить восклицание. Пишем функцию shout, которая принимает функцию и возвращает обёрнутую:
def shout(func):
def wrapper():
result = func()
return result.upper() + '!'
return wrapper
def greet():
return 'привет'
greet = shout(greet)
print(greet())
ПРИВЕТ!
Разбираем: wrapper — это замыкание из прошлого урока, оно помнит исходную func. Строка greet = shout(greet) подменила функцию: имя greet теперь указывает на обёртку. Вызов greet() на самом деле зовёт wrapper, а тот внутри вызывает оригинал и приукрашивает результат. Исходная функция не изменилась ни на символ — поведение добавилось снаружи.
Синтаксис @: та же строка, но короче
Паттерн «объявили функцию, потом подменили её обёрткой» встречается так часто, что для него завели короткую запись — знак @ над объявлением:
def shout(func):
def wrapper():
return func().upper() + '!'
return wrapper
@shout
def greet():
return 'привет'
@shout
def farewell():
return 'пока'
print(greet())
print(farewell())
ПРИВЕТ! ПОКА!
Обёртка, пропускающая аргументы насквозь
Обёртка из примера зовёт func без аргументов, а настоящие функции принимают параметры. Решение — связка *args и **kwargs: обёртка соглашается взять любые аргументы и передаёт их оригиналу в том же виде.
def log_call(func):
def wrapper(*args, **kwargs):
print('Вызов:', func.__name__)
return func(*args, **kwargs)
return wrapper
@log_call
def add(a, b):
return a + b
@log_call
def greet(name):
return f'Привет, {name}!'
print(add(2, 3))
print(greet('Анна'))
Вызов: add 5 Вызов: greet Привет, Анна!
Один декоратор обслужил функции с разными сигнатурами: и add(2, 3), и greet('Анна') прошли через одну и ту же обёртку. Атрибут func.__name__ — имя исходной функции, обычная строка, по ней обёртка пишет, кого вызвали. Обратите внимание на порядок строк вывода: сначала журнал обёртки, потом результат функции.
Декоратор-счётчик
Обёртка может не только пропускать вызов насквозь, но и вести свою бухгалтерию. Счётчик вызовов приклеим к самой обёртке — функции в Python можно добавлять атрибуты, как переменные:
def counted(func):
def wrapper(*args):
wrapper.calls += 1
print('Вызов номер', wrapper.calls)
return func(*args)
wrapper.calls = 0
return wrapper
@counted
def square(n):
return n * n
print(square(3))
print(square(4))
print(square(5))
print('Всего вызовов:', square.calls)
Вызов номер 1 9 Вызов номер 2 16 Вызов номер 3 25 Всего вызовов: 3
Строка wrapper.calls = 0 выполняется один раз при создании обёртки, дальше счёт растёт при каждом вызове. Состояние снова спрятано в замыкание — глобальная переменная не понадобилась.
Честное имя: functools.wraps
У обёртки есть цена: подменив функцию, декоратор подменил и её представление. Функция greet теперь втайне зовётся wrapper:
def shout(func):
def wrapper():
return func().upper() + '!'
return wrapper
@shout
def greet():
return 'привет'
print(greet())
print(greet.__name__)
ПРИВЕТ! wrapper
Для вызовов это неважно, а вот для отладки и документации — да: стек ошибок покажет wrapper, справка потеряется. Стандартная библиотека лечит это декоратором wraps из модуля functools: он копирует имя и документацию оригинала на обёртку.
from functools import wraps
def shout(func):
@wraps(func)
def wrapper():
return func().upper() + '!'
return wrapper
@shout
def greet():
return 'привет'
print(greet())
print(greet.__name__)
ПРИВЕТ! greet
Шаблон вырос на две строки — from functools import wraps и @wraps(func) над wrapper — но теперь обёртка ведёт себя как вежливый двойник: работает за оригинал и носит его имя. Пишите @wraps в каждом декораторе сразу, привычка дешёвая, а отладка благодарная.
Замер времени: честная оговорка
import time
def timed(func):
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f'Время: {elapsed:.4f} с')
return result
return wrapper
Это классика жанра: журнал и счётчик из урока можно запускать — их вывод всегда одинаков; таймер печатает число, которое каждый раз новое. Правило простое: то, что декоратор печатает, должно быть предсказуемым, иначе примеры нельзя проверить.
Стек декораторов: кто первый
Над одной функцией можно поставить два декоратора. Применяются они снизу вверх: декоратор ближе к def — первым:
def reverse_text(func):
def wrapper():
return func()[::-1]
return wrapper
def mark(func):
def wrapper():
return func() + '#'
return wrapper
@reverse_text
@mark
def first():
return 'кот'
@mark
@reverse_text
def second():
return 'кот'
print(first())
print(second())
#ток ток#
В первом случае mark отработал раньше: метка приклеилась, потом строка перевернулась — решётка оказалась впереди. Во втором — наоборот. Одна и та же пара декораторов, разный порядок — разный результат: читайте стек снизу вверх, как историю выполнения.
Декораторы, которые вы уже встретите
| Декоратор | Где живёт | Что делает |
|---|---|---|
| @app.route | веб-фреймворки | Связывает адрес страницы с функцией-обработчиком |
| @pytest.fixture | тесты | Готовит данные для тестов |
| @property | классы | Позволяет методу притворяться полем |
| @wraps | functools | Копирует имя и документацию на обёртку |
- логирование вызовов: журнал из блока «логирующий декоратор» — кто, когда и с чем;
- счётчики: сколько раз вызвали функцию, как часто стреляет медленный кусок;
- проверки: обёртка может заранее проверить аргументы и не пустить их в функцию;
- кэш результатов: модуль functools содержит готовый декоратор lru_cache — он помнит ответы на уже заданные вопросы.
Декоратор собран из трёх вещей, которые вы уже знаете: функция как значение, замыкание и *args с **kwargs. Осталось привыкнуть к записи @ и к тому, что имя функции после декорации указывает на обёртку. Когда что-то пойдёт не так — обёртка примет не тот аргумент или забытый return wrapper вернёт None — на помощь придёт следующий урок об ошибках вызова.
Декоратор с параметрами
Иногда декоратору нужны свои настройки: повторить вызов три раза, повторять попытку при ошибке, печатать только при verbose. Тогда в записи появляются скобки — @repeat(times=3) — и внутри появляется ещё один уровень вложенности.
from functools import wraps
def repeat(times):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return [func(*args, **kwargs) for _ in range(times)]
return wrapper
return decorator
@repeat(times=3)
def greet(name):
return f'Привет, {name}!'
print(greet('Анна'))
['Привет, Анна!', 'Привет, Анна!', 'Привет, Анна!']
Три уровня вложенности, и каждый отвечает за своё. repeat — фабрика: она принимает настройку times и возвращает декоратор. decorator — сам декоратор: он принимает функцию и возвращает обёртку. wrapper — обёртка: она выполняет функцию столько раз, сколько велено. Поэтому в вызове и скобки: @repeat(times=3) сначала вызывает repeat и получает декоратор, а тот уже применяется к greet. У @shout скобок не было — фабрика не требовала настроек.
| Что мы хотим от декоратора | Как это делается в обёртке |
|---|---|
| Записать что-то до вызова | печать в начале wrapper |
| Записать что-то после вызова | печать в конце wrapper, перед return |
| Проверить аргументы | if не подходит: поднять ошибку или вернуть другое |
| Изменить аргументы | вызвать func с другими значениями |
| Изменить результат | result = func(...), а вернуть свой |
| Не вызывать функцию вовсе | вернуть заранее посчитанное — так работает lru_cache |
Ограничение одно, и оно важное: декоратор не может изменить тело функции. Он видит только аргументы и результат — всё, что происходит внутри, ему недоступно. Если логике нужен доступ к внутреннему состоянию объекта, это уже не декоратор, а обычный метод.
Декоратор на методе класса
Декораторы применяют и к методам — и там есть одна деталь, на которой спотыкаются: объект self. Спасает всё та же универсальная сигнатура обёртки.
from functools import wraps
def logged(func):
@wraps(func)
def wrapper(*args, **kwargs):
print('Вызов:', func.__name__)
return func(*args, **kwargs)
return wrapper
class Report:
@logged
def build(self, name):
return f'отчёт {name}'
report = Report()
print(report.build('июнь'))
Вызов: build отчёт июнь
self попал в args вместе с остальными аргументами — обёртка ничего не знает о классах и просто пересылает всё насквозь. Если бы она была написана с фиксированной сигнатурой wrapper(name), то self занял бы место name, и метод сломался бы с невнятной ошибкой. Общее правило для обёрток: принимать *args, **kwargs и не угадывать сигнатуру.
Декоратор-проверка
Ещё один полезный класс декораторов — те, что не дают функции выполниться с плохими данными. Проверка живёт в обёртке, а сама функция остаётся чистой: она получает хорошие значения и работает с ними.
from functools import wraps
def positive(func):
@wraps(func)
def wrapper(*args, **kwargs):
for value in args:
if value <= 0:
return 'ошибка: нужно положительное число'
return func(*args, **kwargs)
return wrapper
@positive
def square(n):
return n * n
print(square(5))
print(square(-2))
25 ошибка: нужно положительное число
Плохое значение остановилось на обёртке, функция square даже не вызвалась. В рабочем коде вместо возврата строки здесь подняли бы исключение — но для примера мы обошлись строкой, чтобы обе ветки были видны. Такие проверки экономят время: вместо падения в глубине расчёта вы получаете понятное сообщение на входе.
Сначала предскажи ответ в голове — это главный навык программиста.
def loud(func):
def wrapper():
return func() + '!'
return wrapper
def say():
return 'привет'
say = loud(say)
print(say())
def log_call(func):
def wrapper(*args):
print('->', func.__name__)
return func(*args)
return wrapper
@log_call
def add(a, b):
return a + b
print(add(2, 3))
from functools import wraps
def shout(func):
@wraps(func)
def wrapper():
return func().upper()
return wrapper
@shout
def greet():
return 'привет'
print(greet())
print(greet.__name__)
1. Что такое декоратор?
2. Запись @shout над def greet эквивалентна какой строке?
3. Зачем в обёртке пишут *args, **kwargs?
4. Что напечатает greet.__name__ после декорации без functools.wraps?
5. Два декоратора в стеке — @reverse_text над @mark. Какой применится первым?
6. Фабрика декоратора забыла строку return wrapper. Что произойдёт при вызове декорированной функции?
7. Сколько уровней вложенности нужно декоратору с параметрами, как @repeat(times=3)?
8. Что вернёт greet('Анна') у функции, декорированной @repeat(times=3)?
9. Почему обёртку декоратора пишут с *args, **kwargs?
Напиши декоратор tagged, который обрамляет результат функции в квадратные скобки, и декорируй им greet.
Что такое декоратор в Python простыми словами?
Функция-упаковщик: принимает вашу функцию и возвращает обёртку, которая делает что-то до и после вызова — пишет журнал, считает вызовы, замеряет время. Запись @dec над def — короткая форма присваивания func = dec(func), новой магии в языке нет.
Урок 15: замыканияЧто означает символ @ перед функцией?
Он ставит над функцией декоратор: @shout над def greet означает greet = shout(greet). Декоратор применяется один раз при объявлении, дальше имя функции указывает на обёртку, а каждый вызов идёт через неё.
Зачем нужен functools.wraps?
Обёртка подменяет имя, документацию и подпись исходной функции — стек ошибок и справка показывают wrapper. Декоратор @wraps(func) из модуля functools копирует метаданные оригинала на обёртку; его пишут в каждом декораторе по привычке.
Почему декоратор пишут без скобок — @shout, а не @shout()?
@shout — вызов фабрики с функцией в аргументе, скобки уже не нужны. Форма @shout() нужна декораторам с параметрами, которые возвращают фабрику, — это следующий уровень, до него доберётесь, когда базовый шаблон станет привычным.
Как написать декоратор с параметрами, например @repeat(times=3)?
Три уровня вложенности: внешняя функция принимает настройку и возвращает декоратор, декоратор принимает функцию и возвращает обёртку, а обёртка работает с *args, **kwargs. В записи поэтому появляются скобки: @repeat(times=3) сначала вызывает repeat.
Можно ли декорировать методы класса?
Да, и обёртка должна принимать *args, **kwargs — тогда self попадает в args и пересылается насквозь. С фиксированной сигнатурой wrapper(name) первый позиционный аргумент занял бы место self, и метод сломался бы.
Что такое functools.lru_cache?
Готовый декоратор из стандартной библиотеки: он запоминает результаты уже выполненных вызовов и не считает их повторно. Для чистых функций с одними и теми же аргументами это заметно ускоряет код.
Как декоратор может запретить вызов функции?
Проверить аргументы в обёртке и не вызывать func вовсе — вернуть сообщение об ошибке или поднять исключение. Функция при этом остаётся чистой: она работает только с хорошими значениями и не знает про проверку.
Понравился урок? Сошлитесь на него
«Декоратор — это функция, которая принимает другую функцию и возвращает новую: поведение добавляется без правки исходного кода.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Функции · Урок 13
Функция как значение в Python: передача функций
map и sorted уже принимали функции в аргументах — теперь разберём приём целиком: функция такое же значение, как число или строка.
Функции · Урок 15
Замыкания в Python: функция с памятью
Функция, которая помнит место своего рождения: делаем счётчик на замыкании, разбираем nonlocal и ловим классическую ловушку с циклом.
Функции · Урок 10
*args в Python: сколько аргументов угодно
Функция, которой нельзя заранее сказать, сколько значений придет: два числа, пять или ни одного. Разбираем звёздочку: упаковку в кортеж и распаковку при вызове.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
ООП · Урок 17
Декораторы: функции над функциями
Декоратор — функция, которая принимает функцию и возвращает новую, добавляя поведение без правки исходного кода. Соберём декоратор с нуля: от замыкания и синтаксиса @ до functools.wraps, декораторов для методов и декораторов с параметрами.
декоратор pythonpython декораторы что это
NumPy · Урок 1
Что такое NumPy и как установить через pip: первый массив ndarray
Первый массив ndarray: создаём, сравниваем со списком, разбираем dtype и shape — и ускоряем сумму миллиона чисел примерно в сто раз.
что такое numpy простыми словами
Функции · Урок 1
Функции в Python: первая def
Первая функция: задаём блоку кода имя через def и вызываем по скобкам. Разбираем объявление на части — имя, двоеточие, тело с отступом — и видим, зачем функция вместо копипасты.
функции в pythonpython функции