Справочник по Python · модуль 6 · функции и модули
Глава 6.14
Декораторы:
введение
Декоратор добавляет функции новое поведение, не трогая её код. Он принимает функцию, оборачивает её в другую и возвращает обёртку — а строка @decorator над определением просто записывает эту замену коротко.
- Категория
- Функции
- Уровень
- Продвинутый
- Связанные темы
- Функции высшего порядка, аргументы, возврат значений
- Зачем это знать
- Добавлять логирование, замеры и проверки без правки самих функций
§1Задача, которую решают декораторы
Представьте два десятка функций, и в каждую нужно добавить одно и то же: записать в журнал момент вызова, замерить время работы, проверить права доступа, запомнить результат. Вписывать эти строки в каждую функцию — значит двадцать раз повторить один код и двадцать раз смешать основную задачу функции с посторонней.
Декоратор — инструмент, позволяющий изменить поведение функции, не меняя её кода. Дополнительные действия описываются один раз, а функции остаются такими, какими были.
§2Функция — обычный объект
Всё устройство декораторов держится на одном свойстве языка: в Python функции — объекты первого класса. Их можно присваивать именам, складывать в списки, передавать и возвращать — как числа или строки.
def say_hello(): print("Привет") greet = say_hello # без скобок — не вызов, а ссылка greet()
Привет
Скобки означают вызов. Имя без скобок — сама функция как объект; именно её и получает декоратор.
§3Функцию можно передать в функцию
Это уже знакомо по главе 6.8 о функциях высшего порядка.
def call_twice(func): func() func() call_twice(say_hello)
Привет Привет
§4Функцию можно вернуть из функции
Вторая половина конструкции: функция, объявленная внутри другой, может быть возвращена наружу — вместе с запомненными значениями окружения.
def make_greeter(name): def greeter(): print("Привет,", name) return greeter # возвращаем функцию, не вызывая её hello_anya = make_greeter("Аня") hello_anya()
Привет, Аня
Функция, которая помнит переменные из места своего создания, называется замыканием. Декоратор — это ровно такая конструкция: он принимает функцию и возвращает замыкание, помнящее её.
§5Обёртывание функции
Обёртывание — создание новой функции, которая добавляет что-то к существующей, не изменяя её исходный код. Новая функция выполняет свои действия до и после вызова исходной, а сама исходная о происходящем ничего не знает.
| Участник | Роль |
|---|---|
| исходная функция | делает основную работу, остаётся без изменений |
обёртка (wrapper) | добавляет действия до и после, вызывает исходную |
| декоратор | принимает исходную функцию и возвращает обёртку |
§6Первый декоратор вручную
Соберём всё вместе — пока без специального синтаксиса.
def my_decorator(func): def wrapper(): print("До вызова функции") func() print("После вызова функции") return wrapper def say(): print("Основное действие") say = my_decorator(say) # имя say теперь указывает на wrapper say()
До вызова функции Основное действие После вызова функции
Функция say не изменилась ни на строку. Изменилось лишь то, на что указывает имя say: теперь это обёртка, которая внутри себя вызывает прежнюю функцию.
- добавлено декоратором
- исходный код, к которому никто не притрагивался
Схема объясняет и само слово «обёртка»: исходная функция оказывается внутри новой, как ядро внутри скорлупы. Снаружи по-прежнему вызывают say() — и не догадываются, что попадают уже не туда, куда попадали раньше.
§7Синтаксис @
Строка say = my_decorator(say) стоит далеко от определения функции, и читатель кода легко её пропустит. Python предлагает короткую запись.
@my_decorator def say(): print("Основное действие") say()
До вызова функции Основное действие После вызова функции
Теперь дополнительное поведение видно прямо над функцией — одной строкой, которую невозможно не заметить.
§8Что именно делает знак @
Никакой магии здесь нет: @ — сокращение для присваивания.
@decorator def function():
def function(): == ...
... function = decorator(function)
Декоратор получает функцию, возвращает что-то новое, и это новое записывается под прежним именем. Отсюда следует важное: декоратор обязан вернуть функцию. Если он вернёт None — а так будет, если забыть return wrapper, — имя станет равно None, и вызов упадёт с TypeError.
§9Аргументы: *args и **kwargs
Обёртка из §6 умеет оборачивать только функции без аргументов: wrapper() объявлен без параметров. Чтобы декоратор подходил любой функции, обёртка принимает аргументы в общем виде — распаковкой из главы 6.6.
def logged(func): def wrapper(*args, **kwargs): print("Вызов", func.__name__, "с", args, kwargs) result = func(*args, **kwargs) print("Результат:", result) return result return wrapper @logged def add(a, b): return a + b total = add(2, 3) print(total)
Вызов add с (2, 3) {}
Результат: 5
5*args, **kwargs в объявлении обёртки собирают любые аргументы, а те же звёздочки при вызове func раздают их обратно. Такой декоратор годится и для add(a, b), и для функции с десятком именованных параметров.
§10Не потерять результат
Строка return result в предыдущем примере — не украшение. Внешний мир теперь вызывает обёртку, и то, что она вернёт, и станет результатом. Забыть return — значит превратить любую функцию в возвращающую None.
def broken(func): def wrapper(*args, **kwargs): func(*args, **kwargs) # результат вычислен и выброшен return wrapper @broken def square(x): return x * x print(square(4))
None
Ошибка коварна тем, что программа не падает: она молча начинает работать с None — и упадёт много позже, в другом месте, с сообщением из главы 6.13.
§11Пример: замер времени
Классическая задача для декоратора: узнать, сколько работает функция, ничего в ней не меняя.
import time def timed(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 @timed def slow_sum(n): return sum(range(n)) slow_sum(1000000)
slow_sum: 0.008 c
Чтобы замерить другую функцию, достаточно поставить над ней ту же строку @timed. Чтобы перестать замерять — убрать её. Код самих функций при этом не трогают.
§12Обёртка подменяет имя функции
У подмены есть побочный эффект: снаружи теперь видна обёртка, и представляется она своим именем.
print(add.__name__)
wrapper
Это мешает и в сообщениях об ошибках, и в справке. Лечится одной строкой из стандартной библиотеки.
import functools def logged(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @logged def multiply(a, b): return a * b print(multiply.__name__)
multiply
functools.wraps — сам декоратор, который копирует имя, строку документации и другие служебные атрибуты исходной функции на обёртку. В рабочем коде его ставят всегда.
§13Когда декоратор срабатывает
Декоратор выполняется один раз — в момент объявления функции, а не при каждом вызове. При объявлении Python вызывает my_decorator(say) и запоминает результат под именем say. При каждом последующем вызове работает уже тело обёртки.
- Python читает
defи создаёт исходную функцию. - Немедленно вызывает декоратор, передав ему эту функцию.
- Записывает возвращённую обёртку под именем функции.
- Каждый дальнейший вызов по этому имени идёт в обёртку.
Отсюда практический вывод: тяжёлую подготовку (открыть журнал, прочитать настройки) можно делать в теле самого декоратора — она произойдёт однократно. А то, что должно повторяться при каждом вызове, помещают в обёртку.
§14Частые ошибки
| Что написано | Что произойдёт | Как правильно |
|---|---|---|
забыт return wrapper | имя функции станет None, вызов даст TypeError | декоратор всегда возвращает функцию |
забыт return result в обёртке | функция молча начнёт возвращать None | вернуть результат вызова func |
return wrapper() со скобками | функция выполнится сразу при объявлении, а имя получит её результат | return wrapper без скобок |
def wrapper(): без параметров | TypeError при вызове функции с аргументами | def wrapper(*args, **kwargs): |
обёртка без functools.wraps | __name__ и документация подменяются на wrapper | поставить @functools.wraps(func) |
@my_decorator() со скобками у простого декоратора | TypeError: вызвали декоратор без функции | @my_decorator без скобок |
§15Проверьте себя
Что принимает и что возвращает декоратор?
Принимает функцию, возвращает функцию — обычно обёртку, которая вызывает исходную и добавляет действия до и после.
Во что разворачивается строка @decorator?
В присваивание function = decorator(function) сразу после определения функции.
Зачем обёртке *args, **kwargs?
Чтобы декоратор подходил функциям с любым набором аргументов: обёртка принимает их в общем виде и передаёт исходной функции.
Почему обёртка должна возвращать результат?
Потому что снаружи вызывается именно она. Без return результат исходной функции будет потерян и вызов вернёт None.
Что делает functools.wraps?
Переносит на обёртку имя, документацию и служебные атрибуты исходной функции, чтобы та не «исчезала» из сообщений и справки.
Сколько раз выполняется код декоратора?
Один раз — при объявлении функции. Тело обёртки выполняется при каждом вызове.
§16Шпаргалка
def deco(func): def wrapper(*a, **kw): return func(*a, **kw) return wrapper@decodef f(): ...f = deco(f)@functools.wraps(func)Итог главы
Декоратор — функция, принимающая функцию и возвращающая новую, обычно обёртку с дополнительным поведением. Строка @decorator над определением означает лишь function = decorator(function), и вся конструкция держится на том, что функции в Python — обычные объекты, которые можно передавать и возвращать. Работоспособной обёртку делают три детали: *args, **kwargs для любых аргументов, return результата и functools.wraps для сохранения имени. С этим набором логирование, замеры и проверки добавляются к любой функции одной строкой и снимаются так же легко.