Модуль 6 · Функции и модули

Справочник по Python · модуль 6 · функции и модули

Глава 6.14

6.14

Декораторы:
введение

Декоратор добавляет функции новое поведение, не трогая её код. Он принимает функцию, оборачивает её в другую и возвращает обёртку — а строка @decorator над определением просто записывает эту замену коротко.

Категория
Функции
Уровень
Продвинутый
Связанные темы
Функции высшего порядка, аргументы, возврат значений
Зачем это знать
Добавлять логирование, замеры и проверки без правки самих функций

§1Задача, которую решают декораторы

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

Декоратор — инструмент, позволяющий изменить поведение функции, не меняя её кода. Дополнительные действия описываются один раз, а функции остаются такими, какими были.

§2Функция — обычный объект

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

main.pyимя функции — обычная переменная
def say_hello():
    print("Привет")

greet = say_hello      # без скобок — не вызов, а ссылка
greet()
Выводдва имени, одна функция
Привет

Скобки означают вызов. Имя без скобок — сама функция как объект; именно её и получает декоратор.

§3Функцию можно передать в функцию

Это уже знакомо по главе 6.8 о функциях высшего порядка.

main.pyфункция как аргумент
def call_twice(func):
    func()
    func()

call_twice(say_hello)
Выводвызвали дважды
Привет
Привет

§4Функцию можно вернуть из функции

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

main.pyвнутренняя функция помнит name
def make_greeter(name):
    def greeter():
        print("Привет,", name)
    return greeter          # возвращаем функцию, не вызывая её

hello_anya = make_greeter("Аня")
hello_anya()
Выводзначение name сохранилось
Привет, Аня

Функция, которая помнит переменные из места своего создания, называется замыканием. Декоратор — это ровно такая конструкция: он принимает функцию и возвращает замыкание, помнящее её.

§5Обёртывание функции

Обёртывание — создание новой функции, которая добавляет что-то к существующей, не изменяя её исходный код. Новая функция выполняет свои действия до и после вызова исходной, а сама исходная о происходящем ничего не знает.

УчастникРоль
исходная функцияделает основную работу, остаётся без изменений
обёртка (wrapper)добавляет действия до и после, вызывает исходную
декораторпринимает исходную функцию и возвращает обёртку

§6Первый декоратор вручную

Соберём всё вместе — пока без специального синтаксиса.

main.pyзамена функции на обёртку
def my_decorator(func):
    def wrapper():
        print("До вызова функции")
        func()
        print("После вызова функции")
    return wrapper

def say():
    print("Основное действие")

say = my_decorator(say)      # имя say теперь указывает на wrapper
say()
Выводосновное действие оказалось в середине
До вызова функции
Основное действие
После вызова функции

Функция say не изменилась ни на строку. Изменилось лишь то, на что указывает имя say: теперь это обёртка, которая внутри себя вызывает прежнюю функцию.

Схемачто теперь стоит за именем say
вызов say() wrapper — то, на что теперь указывает имя say print("До вызова функции") исходная функция say — не изменилась print("Основное действие") print("После вызова функции") результат возвращается вызывающему коду
  • добавлено декоратором
  • исходный код, к которому никто не притрагивался

Схема объясняет и само слово «обёртка»: исходная функция оказывается внутри новой, как ядро внутри скорлупы. Снаружи по-прежнему вызывают say() — и не догадываются, что попадают уже не туда, куда попадали раньше.

§7Синтаксис @

Строка say = my_decorator(say) стоит далеко от определения функции, и читатель кода легко её пропустит. Python предлагает короткую запись.

main.pyто же самое через @
@my_decorator
def say():
    print("Основное действие")

say()
Выводрезультат идентичен §6
До вызова функции
Основное действие
После вызова функции

Теперь дополнительное поведение видно прямо над функцией — одной строкой, которую невозможно не заметить.

§8Что именно делает знак @

Никакой магии здесь нет: @ — сокращение для присваивания.

Эквивалентдве записи означают одно
@decorator                  def function():
def function():        ==       ...
    ...                     function = decorator(function)

Декоратор получает функцию, возвращает что-то новое, и это новое записывается под прежним именем. Отсюда следует важное: декоратор обязан вернуть функцию. Если он вернёт None — а так будет, если забыть return wrapper, — имя станет равно None, и вызов упадёт с TypeError.

§9Аргументы: *args и **kwargs

Обёртка из §6 умеет оборачивать только функции без аргументов: wrapper() объявлен без параметров. Чтобы декоратор подходил любой функции, обёртка принимает аргументы в общем виде — распаковкой из главы 6.6.

main.pyуниверсальная обёртка
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.

main.pyобёртка вызвала функцию, но результат не вернула
def broken(func):
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)      # результат вычислен и выброшен
    return wrapper

@broken
def square(x):
    return x * x

print(square(4))
Вывод16 потерялось внутри обёртки
None

Ошибка коварна тем, что программа не падает: она молча начинает работать с None — и упадёт много позже, в другом месте, с сообщением из главы 6.13.

§11Пример: замер времени

Классическая задача для декоратора: узнать, сколько работает функция, ничего в ней не меняя.

main.pyизмерение длительности вызова
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Обёртка подменяет имя функции

У подмены есть побочный эффект: снаружи теперь видна обёртка, и представляется она своим именем.

main.pyфункция потеряла имя
print(add.__name__)
Выводвместо add — wrapper
wrapper

Это мешает и в сообщениях об ошибках, и в справке. Лечится одной строкой из стандартной библиотеки.

main.pyfunctools.wraps переносит имя и описание
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. При каждом последующем вызове работает уже тело обёртки.

  1. Python читает def и создаёт исходную функцию.
  2. Немедленно вызывает декоратор, передав ему эту функцию.
  3. Записывает возвращённую обёртку под именем функции.
  4. Каждый дальнейший вызов по этому имени идёт в обёртку.

Отсюда практический вывод: тяжёлую подготовку (открыть журнал, прочитать настройки) можно делать в теле самого декоратора — она произойдёт однократно. А то, что должно повторяться при каждом вызове, помещают в обёртку.

§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 для сохранения имени. С этим набором логирование, замеры и проверки добавляются к любой функции одной строкой и снимаются так же легко.

Глава 6.14 · Декораторы: введение · Справочник по Python Автор: Макаров Максим Николаевич