В этом руководстве по декораторам мы рассмотрим, что они собой представляют, как их создавать и использовать. По определению, декоратор – это функция, которая принимает другую функцию и расширяет поведение последней, не изменяя ее явным образом. В этом туториале мы постараемся понять, что это значит и как реализуется.
Статья является сокращенным переводом публикации Гейра Арне Хьелле Primer on Python Decorators. Оригинальный код из этой статьи доступен в GitHub-репозитории.
Так как в коде много примеров, мы также подготовили Jupyter-блокнот с текстом перевода и адаптированным кодом, чтобы его было проще запускать интерактивно (о работе с Jupyter Библиотека программиста рассказывала в статье JupyterLab и Jupyter Notebook — мощные инструменты Data Science).
Если вам интересна интерактивность, но вы ничего дополнительно не хотите устанавливать, запустите его в Colab: нажимая последовательно [Shift]-[Enter]
, можно запускать сопроводительный код в ячейках интерактивного блокнота на удаленном сервере и при желании экспериментировать с ним.
1. Предварительные соображения: функции
Прежде чем начать разбираться в декораторах, немного поговорим о важных для их понимания свойствах функций.
1.1. Передача функции в качестве аргумента
В Python функции можно передавать и использовать в качестве аргументов, как и любой другой объект. Рассмотрим следующие три функции:
Здесь say_hello()
и be_awesome()
– обычные функции, которые получают строковую переменную name
. Функция greet_vanya()
в качестве аргумента получает другую функцию, например say_hello()
или be_awesome()
:
При передаче в качестве аргумента имя функции указывается без скобок – передаётся только ссылка на функцию. Сама функция не выполняется, пока не будет вызвана функция greet_vanya()
.
1.2. Внутренние функции
Функции, определенные внутри других функций, называются внутренними (inner functions). Пример функции с двумя внутренними функциями:
Что произойдёт при вызове функции parent()
? Остановитесь, чтобы подумать. Вывод будет следующим:
Обратите внимание, что порядок, в котором определены внутренние функции, не имеет значения. Печать происходит только при вызове функции.
Внутренние функции не определены, пока не вызвана родительская функция. То есть они локально ограничены parent()
и существуют только внутри нее, как локальные переменные. При вызове функции first_child()
за пределами parent()
мы получим ошибку:
1.3. Возврат функций из функций
Python позволяет использовать функции в качестве возвращаемых значений. В следующем примере возвращается одна из внутренних функций внешней функции parent()
:
В инструкции return
возвращается ссылка на функцию, то есть имя функции указывается без скобок (иначе бы возвращался результат выполнения функции).
В приведенном примере first
и second
– переменные, в которые были записаны ссылки на локальные функции first_child()
и second_child()
внутри функции parent()
. Теперь first
и second
можно использовать как обычные функции, хотя функции, на которые они указывают, недоступны напрямую:
Обратите внимание, что в предыдущем разделе о внутренних функциях мы не имели доступа к first_child()
. В последнем же примере мы получили ссылку на каждую функцию и можем их вызывать в будущем.
2. Простые декораторы 💅
2.1. Общая идея: используем знания о функциях
Теперь, когда мы увидели, что функции в Python похожи на любые другие объекты, нам будет проще понять «магию» декораторов. Начнём с искусственного примера, поясняющего идею:
Знаете, что произойдёт при вызове say_whee()
?
Чтобы понять, что происходит, оглянемся на предыдущие примеры. Мы просто применяем всё, что узнали до сих пор. Декорирование происходит в последней строчке:
Мы передаем в функцию my_decorator()
ссылку на функцию say_whee
. В my_decorator()
есть внутренняя функция wrapper()
, ссылка на которую возвращается в инструкции return
внешней функции. В результате мы передали в my_decorator()
в качестве аргумента ссылку на одну функцию, а назад получили ссылку на её функцию-обёртку.
Теперь имя say_whee
указывает на внутреннюю функцию wrapper
:
Однако wrapper()
содержит ссылку на оригинал say_whee()
и вызывает эту функцию между двумя вызовами print()
.
Добавим динамики. Рассмотрим второй пример, иллюстрирующий динамическое поведение декораторов. Сделаем так, чтобы наша функция кричала "Ура!" только в дневное время.
Декорированная функция say_whee()
будет выводить "Ура"
только, если она запущена в интервале c 8:00 до 22:00 (чтобы проверить разницу в поведении, «подкрутите стрелки» ⏰).
2.2. Немного синтаксического сахара! 🍭
То, как мы декорировали say_whee()
, прямо скажем, выглядит неуклюже. В последнем примере мы три раза использовали имя say_whee
: при определении функции-оригинала, при передаче ссылку в функцию not_during_the_night()
и при переопределении имени для создания ссылки на декоратор.
Чтобы не заниматься такими глупостями, в Python можно создать декоратор с помощью символа @
. Следующий код эквивалентен первому рассмотренному примеру:
То есть инструкция @my_decorator,
идущая перед определением функции say_whee()
эквивалентна инструкции say_whee = my_decorator(say_whee)
.
2.3. Повторное использование декораторов
Как и любую другую функцию, декоратор можно поместить в отдельный модуль и использовать для различных целей. К примеру, создадим файл decorators.py
со следующим содержанием:
Теперь импортируем функцию из модуля и используем как декоратор:
Вызвав декорированную функцию, мы получаем, что оригинальная функция выполняется дважды:
2.4. Декорирование функций, принимающих аргументы 📥
Пусть у нас есть функция, принимающая аргументы. Можем ли мы ее декорировать? Попробуем:
К сожалению, запуск кода вызовет ошибку:
Проблема в том, что внутренняя функция декоратора wrapper_do_twice()
не принимает аргументов. Нужно добавить их обработку. Перепишем decorators.py
следующим образом:
Теперь внутренняя функция декоратора принимает любое число аргументов и пересылает их декорируемой функции. Так обе декорированные функции будут работать корректно:
2.5. Возвращение значения из декорированных функций 📤
В декораторе можно описать, что делать со значением, возвращаемым декорированной функцией:
Попытаемся использовать декорированную функцию:
К сожалению, декоратор «съел» значение, возвращаемое оригинальной функцией. Поскольку wrapper_do_twice()
в явном виде не возвращает никакое значение, вызов в return_greeting("Адам")
в конечном итоге вернул None
.
Сделаем так, чтобы внутренняя функция декоратора возвращала значение декорированной функции. Поправим файл decorators.py
:
Проверим, как всё работает теперь:
2.6. Интроспекция: «кто ты такой, в самом деле?» 🕵
Большое удобство в работе с Python – его способность к интроспекции. У объекта есть доступ к собственным атрибутам. К примеру, у функции можно спросить её имя и вызвать документацию:
Интроспекция работает и для пользовательских функций:
Как видим, в результате декорирования функция say_whee()
запуталась в собственной идентичности. Теперь она сообщает, что является внутренней функцией wrapper_do_twice
в модуле decorators
. Хотя это технически верно, эта информация не очень полезна.
Чтобы исправить ситуацию, декоратор должен использовать... специальный декоратор @functools.wraps
. Этот декоратор позволяет сохранить информацию об исходной функции. Снова уточним модуль decorators.py
:
В самой декорируемой функции ничего менять не придется:
Гораздо лучше! Теперь у функции say_whee()
не наступает амнезии после декорирования.
3. Несколько примеров из реального мира🎈
Посмотрим на несколько полезных примеров декораторов. Как вы заметите, схема применения декораторов будет соответствовать тому паттерну, что мы получили в результате наших рассуждений:
Этот блок кода является хорошим шаблоном для создания более сложных декораторов.
decorators.py
. Напомним, что файлы с программным кодом этого туториала доступны в GitHub-репозитории.3.1. Декоратор для тайминга кода ⌚
Начнем с создания декоратора @timer
. Он будет измерять время выполнения функции и выводить результат в консоль:
Декоратор сохраняет текущее время в переменной start_time
непосредственно перед запуском декорируемой функции. Это значение впоследствии вычитается из текущего значения end_time
после выполнения функции. Полученная разность run_time
передается в форматированную строку. Пара примеров:
@timer
отлично подходит, если вы хотите получить представление о времени выполнения функции. Для более точных замеров используйте модуль стандартной библиотеки timeit
. Мы рассказывали о нём в публикации Назад в будущее: практическое руководство по путешествию во времени с Python.3.2. Отладочный декоратор 🐞🕵
Следующий декоратор @debug
будет выводить аргументы, с которыми вызвана функция, а также возвращаемое функцией значения:
Отмеченные комментариями строки соответствуют следующим операциям:
- Создание списка позиционных аргументов:
repr()
используется для строкового представления каждого аргумента. - Создание списка аргументов, передающихся по ключу: f-строка форматирует каждый элемент в формате
key=value
со спецификатором!r
, соответствующимrepr()
. - Списки аргументов объединяются в общую подпись, элементы разделены запятыми.
- Возвращаемое значение выводится после исполняемой функции.
Давайте посмотрим, как декоратор работает на практике, применив его к простой функции с одним позиционным аргументов и одним аргументом, передаваемым по ключу:
Тестируем:
Пример не сразу покажется полезным – декоратор @debug
просто повторяет то, что мы ему прислали. Но он гораздо эффективнее, если его применить к небольшим вспомогательным функциям. Следующий пример иллюстрирует аппроксимацию для нахождения числа или вычисления числа e.
Этот пример показывает, как вы можете применить декоратор к уже определенной функции. Аппроксимация нахождения числа е
основана на следующем разложении в ряд:
При вызове функции approximate_e()
мы увидим @debug
за работой:
Видно, что сложив только пять первых членов ряда, мы получаем довольно близкое значение к числу e
.
3.3. Замедление кода 🐌
Следующий пример вряд ли покажется полезным. Зачем нам вообще замедлять код Python? Например, мы хотим ограничить частоту, с которой функция проверяет обновление веб-ресурса. Декоратор @slow_down
будет выжидать одну секунду перед запуском декорируемой функции:
Чтобы увидеть результат действия декоратора, запустите пример:
Декоратор @slow_down
спит всегда лишь одну секунду. Позднее мы увидим, как передавать декоратору аргумент, чтобы контролировать его скорость.
3.4. Регистрация плагинов
Вообще декораторы не обязаны «оборачивать» функцию, которую они декорируют. Они могут просто регистрировать то, что функция существует и возвращать на нее ссылку. Это может использоваться для создания легковесной архитектуры:
В приведенном примере декоратор @register
просто добавляет ссылку на декорируемую функцию в глобальный словарь PLUGINS
. Никакой внутренней функции у декоратора нет, оригинальная функция возвращается немодифицированной, поэтому нет необходимости использовать @functools.wraps
.
Функция randomly_greet()
случайным образом выбирает, какую из зарегистрированных функций использовать для поздравления. Удобство состоит в том, что словарь PLUGINS
уже содержит ссылку для каждой функции, к которой был применен декоратор @register
:
3.5. Залогинился ли пользователь? 🤔
Последний пример перед тем, как перейти к некоторым более изящным декораторам обычно используется при работе с веб-фреймворками. В этом примере мы используем Flask для настройки веб-страницы /secret
– она должна быть видна только пользователям, вошедшим в систему:
4. Декораторы поинтереснее 👑
До сих пор мы видели довольно простые декораторы – нам нужно было понять, как они работают. Вы можете передохнуть и попрактиковаться в применении декораторов, чтобы позднее вернуться к этому разделу, посвященному продвинутым концепциям.
На текущий момент наш файл decorators.py
имеет следующее содержание:
4.1. Декорирование классов 👨🏫️
Есть два способа применения декораторов к классам. Первый способ похож на то, что мы делали с функциями, – декорировать методы класса.
@classmethod
, @staticmethod
и @property
. Первые два используются, чтобы определить методы внутри пространства имен классов, не связанные с конкретным экземпляром класса. Декоратор @property
используется для настройки геттеров и сеттеров атрибутов класса. Давайте определим класс, в котором декорируем некоторые из методов с помощью вышеописанных декораторов @debug
и @timer
:
Воспользуемся классом, чтобы увидеть действие декораторов:
Другой подход – декорировать классы целиком. Написание декоратора класса очень похоже на написание декоратора функции. Разница лишь в том, что декоратор в качестве аргумента получит класс, а не функцию. Однако когда мы применяем декораторы функций к классам, их эффект может оказаться не таким, как предполагалось. В следующем примере мы применили декоратор @timer
к классу:
Декорирование класса не приведет к декорированию его методов. В результате @timer
измерит только время создания экземпляра класса:
Позднее мы покажем примеры правильного декорирования классов.
4.2. Вложенные декораторы 🎊
К функции можно применить несколько декораторов, накладывая их действие друг на друга:
В этом случае к функции будет применен сначала декоратор @do_twice
, потом @debug
:
Посмотрим, что будет, если поменять порядок вызова декораторов:
4.3. Декораторы, принимающие аргументы 📬
Иногда полезно передавать декораторам аргументы, чтобы управлять их поведением. Например, @do_twice
может быть расширен до декоратора @repeat(num_times
). Число повторений декорируемой функции можно было бы указать в качестве аргумента:
Подумаем, как добиться такого поведения. Обычно декоратор создает и возвращает внутреннюю функцию-обертку. Мы могли бы дополнительно «обернуть» ее поведение с помощью другой внутренней функции:
Немного похоже на фильм Кристофера Нолана «Начало», но мы просто поместили один шаблон многократно выполняющего функцию декоратора в другой декоратор и добавили обработку значения аргумента.
Давайте проверим, работает ли, как задумано:
4.4. «И того, и другого, и можно без хлеба!»
Немного потрудившись, мы можем определить декоратор, который можно использовать как с аргументами, так и без них.
Поскольку ссылка на декорируемую функцию передается напрямую только в случае, если декоратор был вызван без аргументов, ссылка на функцию должна быть необязательным аргументом. То есть все аргументы декоратора должны передаваться по ключу. Для этого мы можем применить специальный синтаксис (*
), указывающий, что все остальные аргументы передаются по ключу:
Здесь аргумент _func
действует как маркер, отмечающий, был ли декоратор вызван с аргументами или без них.
Если функция декоратора name будет вызвана без аргументов, декорируемая функция будет передана как _func
. Если декоратор будет вызван с аргументами, тогда значение _func
останется None
, а передаваемые по ключу аргументы заменят значения по умолчанию. Символ *
в списке аргументов означает, что следующие за ним аргументы не могут быть переданы как позиционные.
То есть в сравнении с предыдущей версией к декоратору добавилось условие if-else
:
4.5. Декораторы, хранящие состояние 💾
Иногда полезно иметь декораторы, отслеживающие состояние. В качестве простого примера мы создадим декоратор, который подсчитывает, сколько раз вызывалась функция.
В следующем разделе мы увидим, как использовать для сохранения состояния классы. Но в простых случаях достаточно декораторов функций:
4.6. Классы в качестве декораторов функций
Обычным способом хранения состояния является использование классов. Перепишем @count_calls
из предыдущего раздела, используя в качестве декоратора класс.
Напомним, что синтаксис декоратора @my_decorator
– это всего лишь более простой способ сказать func = my_decorator(func)
. Если my_decorator
является классом, он должен принять func
в качестве аргумента в методе __init__()
.
Кроме того, класс должен быть вызван так, чтобы он его можно было вызвать вместо декорируемой функции. Для этого в нем должен быть описан метод __call__()
:
Метод __call__()
вызывается всякий раз, когда мы обращаемся к экземпляру класса:
Таким, образом типичная реализация класса декоратора должна содержать __init__ ()
и __call__ ()
:
Метод __init__()
должен хранить ссылку на функцию и может выполнять любую другую необходимую инициализацию.
Метод __call__()
будет вызываться вместо декорированной функции. По сути, он делает то же самое, что и функция wrapper()
в наших предыдущих примерах.
Обратите внимание, что в случае методов классов нужно использовать функцию functools.update_wrapper()
вместо @functools.wraps
.
Декоратор @CountCalls
работает так же, как и в предыдущем разделе:
5. Ещё несколько примеров из реального мира 🧭
Мы прошли долгий путь и узнали, как создаются всевозможные декораторы. Давайте подведем итоги, применив полученные знания для анализа полезных на практике программных конструкций.
5.1. Вновь замедляем код, но уже по-умному 🐢
Наша предыдущая реализация замедлителя кода @slow_down
всегда «усыпляла» декорируемую функцию на одно и то же время. Давайте воспользуемся нашими знаниями о передачи в декоратор аргументов:
Проверим на примере функции countdown()
:
5.2. Создание синглтонов 🗿
Синглтон – это класс с единственным экземпляром. В Python есть несколько часто используемых синглтонов, к примеру: None
, True
и False
. Тот факт, что None является синглтоном, позволяет использовать оператор is
для сравнения объектов с None
. Мы пользовались этим выше:
Оператор is
возвращает True
только для объектов, представляющих одну и ту же сущность.
Описанный ниже декоратор @singleton
превращает класс в одноэлементный, сохраняя первый экземпляр класса в качестве атрибута. Последующие попытки создания экземпляра просто возвращают сохраненный экземпляр:
Как видите, этот декоратор класса следует тому же шаблону, что и наши декораторы функций. Единственное отличие состоит в том, что мы используем cls
вместо func
в качестве имени параметра.
first_one
действительно представляет тот же экземпляр, что и another_one
.
5.3. Кэширование возвращаемых значений ⏳
Декораторы предоставляют прекрасный механизм для кэширования и мемоизации. В качестве примера давайте рассмотрим рекурсивное определение последовательности Фибоначчи:
Хотя реализация и выглядит просто, с производительностью дела обстоят плохо:
Чтобы рассчитать десятое число в последовательности Фибоначчи, в действительности достаточно лишь вычислить предыдущие числа этого ряда. Однако указанная реализация требует выполнения 177 вычислений. И ситуация быстро ухудшается: для 30-го числа потребуется 2.7 млн. операций. Это объясняется тем, что код каждый раз пересчитывает числа последовательности, уже известные из предыдущих этапов.
Обычное решение состоит в том, чтобы находить числа Фибоначчи, используя цикл for
и справочную таблицу. Тем не менее, можно просто добавить к рекурсии кэширование вычислений:
Кэш работает как справочная таблица, поэтому теперь fibonacci()
выполняет необходимые вычисления только один раз:
Заметьте, что при вызове fibonacci(8)
не происходит никаких дополнительных расчетов – все необходимые значения уже найдены и сохранены при вычислении fibonacci(11)
.
5.4. Добавление единиц измерения ⚖️
Следующий пример похож на задачу о регистрации плагинов (функций) – здесь тоже не будет меняться поведение декорированной функции. Вместо этого к атрибутам функции будут добавляться единицы измерения:
В следующем примере вычисляется объем цилиндра по известному радиусу и высоте, указанных в сантиметрах:
Атрибут unit
можно далее использовать по мере необходимости:
Обратите внимание, что подобного поведения можно добиться, используя аннотации функций:
Однако, использовать аннотации для единиц измерения несколько затруднительно, поскольку они обычно используются для статической проверки типов.
5.5. Валидация JSON 🗝
Рассмотрим последний пример практического применения декораторов. Взглянем на следующий обработчик маршрута Flask:
Здесь мы гарантируем, что ключ student_id
является частью запроса. Хотя эта проверка работает, на деле она не относится к самой функции. Кроме того, могут быть другие маршруты, которые используют ту же самую проверку. Итак, давайте абстрагируем всю стороннюю логику с помощью декоратора @validate_json
:
В приведенном выше коде декоратор принимает в качестве аргумента список переменной длины. Каждый аргумент представляет собой ключ, используемый для проверки данных JSON. Функция-обертка проверяет, присутствует ли каждый ожидаемый ключ в данных JSON. Теперь обработчик маршрута может сосредоточиться на своей главной работе – обновлении оценок студентов:
Заключение
Поздравляем, вы дошли до конца статьи! 🎖️
Итак, теперь вы знаете:
- Как создавать декораторы функций и классов.
- Как передавать в декораторы аргументы и возвращать из них значения.
- Зачем в декораторах используется @functools.wraps.
- Как использовать вложенные декораторы.
- Как при помощи декораторов хранить состояния и кэшировать результаты функций.
В определении декораторов нет никакой магии. Обычно всё направлено на создание функции или класса, выступающих в качестве обёртки. Для передачи аргументов применяется обычная нотация *args
и **kwargs
. А использование знака @
представляет лишь синтаксический сахар, облегчающий вызов декораторов.
Декораторы очень удобны, чтобы модифицировать поведение функций и классов, создавать для их обработки дополнительную логику. При этом такие шаблоны модификации легко наслаивать друг на друга с помощью вложенных декораторов. Чтобы снять декорирование, достаточно просто удалить строчку с упоминанием декоратора.
Для ещё более глубокого погружения в декораторы, посмотрите исторический документ PEP 318, а также вики-страницу, посвященную декораторам Python.
Сторонний модуль decorator также поможет вам в создании собственных декораторов. Его документация содержит ещё больше примеров использования декораторов.
Если вам понравилась эта статья, вот ещё три родственных материала по важным темам Python:
- Не изобретать велосипед, или Обзор модуля collections в Python
- Итерируем правильно: 20 приемов использования в Python модуля itertools
- Назад в будущее: практическое руководство по путешествию во времени с Python
Комментарии