Как работают декораторы python
Перейти к содержимому

Как работают декораторы python

  • автор:

Декораторы в Python: понять и полюбить

Обложка: Декораторы в Python: понять и полюбить

Декораторы — один из самых полезных инструментов в Python, однако новичкам они могут показаться непонятными. Возможно, вы уже встречались с ними, например, при работе с Flask, но не хотели особо вникать в суть их работы. Эта статья поможет вам понять, чем являются декораторы и как они работают.

Что такое декоратор?

Новичкам декораторы могут показаться неудобными и непонятными, потому что они выходят за рамки «обычного» процедурного программирования как в Си, где вы объявляете функции, содержащие блоки кода, и вызываете их. То же касается и объектно-ориентированного программирования, где вы определяете классы и создаёте на их основе объекты. Декораторы не принадлежат ни одной из этих парадигм и исходят из области функционального программирования. Однако не будем забегать вперёд, разберёмся со всем по порядку.

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

Как работают функции

Все мы знаем, что такое функции, не так ли? Не будьте столь уверены в этом. У функций Python есть определённые аспекты, с которыми мы нечасто имеем дело, и, как следствие, они забываются. Давайте проясним, что такое функции и как они представлены в Python.

Функции как процедуры

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

Функции как объекты первого класса

В Python всё является объектом, а не только объекты, которые вы создаёте из классов. В этом смысле он (Python) полностью соответствует идеям объектно-ориентированного программирования. Это значит, что в Python всё это — объекты:

  • числа;
  • строки;
  • классы (да, даже классы!);
  • функции (то, что нас интересует).

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

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

И тут в дело вступает функциональное программирование, а вместе с ним — декораторы.

Функциональное программирование — функции высших порядков

В Python используются некоторые концепции из функциональных языков вроде Haskell и OCaml. Пропустим формальное определение функционального языка и перейдём к двум его характеристикам, свойственным Python:

  • функции являются объектами первого класса;
  • следовательно, язык поддерживает функции высших порядков.

Функциональному программированию присущи и другие свойства вроде отсутствия побочных эффектов, но мы здесь не за этим. Лучше сконцентрируемся на другом — функциях высших порядков. Что есть функция высшего порядка? Снова обратимся к Википедии:

Функции высших порядков — это такие функции, которые могут принимать в качестве аргументов и возвращать другие функции.

Если вы знакомы с основами высшей математики, то вы уже знаете некоторые математические функции высших порядков порядка вроде дифференциального оператора d/dx. Он принимает на входе функцию и возвращает другую функцию, производную от исходной. Функции высших порядков в программировании работают точно так же — они либо принимают функцию(и) на входе и/или возвращают функцию(и).

Пара примеров

Раз уж мы ознакомились со всеми аспектами функций в Python, давайте продемонстрируем их в коде:

Здесь мы определили простую функцию. Из фрагмента кода далее вы увидите, что эта функция, как и классы с числами, является объектом в Python:

Как вы заметили, функция hello_world принадлежит типу <class ‘function’> . Это означает, что она является объектом класса function . Кроме того, класс, который мы определили, принадлежит классу type . От этого всего голова может пойти кругом, но чуть поигравшись с функцией type вы со всем разберётесь.

Теперь давайте посмотрим на функции в качестве объектов первого класса.

Мы можем хранить функции в переменных:

Определять функции внутри других функций:

Передавать функции в качестве аргументов и возвращать их из других функций:

Из этих примеров должно стать понятно, насколько функции в Python гибкие. С учётом этого можно переходить к обсуждению декораторов.

Как работают декораторы

Повторим определение декоратора:

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

Раз мы знаем, как работают функции высших порядков, теперь мы можем понять как работают декораторы. Сначала посмотрим на пример декоратора:

Здесь decorator_function() является функцией-декоратором. Как вы могли заметить, она является функцией высшего порядка, так как принимает функцию в качестве аргумента, а также возвращает функцию. Внутри decorator_function() мы определили другую функцию, обёртку, так сказать, которая обёртывает функцию-аргумент и затем изменяет её поведение. Декоратор возвращает эту обёртку. Теперь посмотрим на декоратор в действии:

Магия, не иначе! Просто добавив @decorator_function перед определением функции hello_world() , мы модифицировали её поведение. Однако как вы уже могли догадаться, выражение с @ является всего лишь синтаксическим сахаром для hello_world = decorator_function(hello_world) .

Иными словами, выражение @decorator_function вызывает decorator_function() с hello_world в качестве аргумента и присваивает имени hello_world возвращаемую функцию.

И хотя этот декоратор мог вызвать вау-эффект, он не очень полезный. Давайте взглянем на другие, более полезные (наверное):

Здесь мы создаём декоратор, замеряющий время выполнения функции. Далее мы используем его на функции, которая делает GET-запрос к главной странице Google. Чтобы измерить скорость, мы сначала сохраняем время перед выполнением обёрнутой функции, выполняем её, снова сохраняем текущее время и вычитаем из него начальное.

После выполнения кода получаем примерно такой результат:

К этому моменту вы, наверное, начали осознавать, насколько полезными могут быть декораторы. Они расширяют возможности функции без редактирования её кода и являются гибким инструментом для изменения чего угодно.

Используем аргументы и возвращаем значения

В приведённых выше примерах декораторы ничего не принимали и не возвращали. Модифицируем наш декоратор для измерения времени выполнения:

Вывод после выполнения:

Как вы видите, аргументы декорируемой функции передаются функции-обёртке, после чего с ними можно делать что угодно. Можно изменять аргументы и затем передавать их декорируемой функции, а можно оставить их как есть или вовсе забыть про них и передать что-нибудь совсем другое. То же касается возвращаемого из декорируемой функции значения, с ним тоже можно делать что угодно.

Звёздный Python: где и как используются * и **

Декораторы с аргументами

Мы также можем создавать декораторы, которые принимают аргументы. Посмотрим на пример:

Здесь мы модифицировали наш старый декоратор таким образом, чтобы он выполнял декорируемую функцию iters раз, а затем выводил среднее время выполнения. Однако чтобы добиться этого, пришлось воспользоваться природой функций в Python.

Функция benchmark() на первый взгляд может показаться декоратором, но на самом деле таковым не является. Это обычная функция, которая принимает аргумент iters , а затем возвращает декоратор. В свою очередь, он декорирует функцию fetch_webpage() . Поэтому мы использовали не выражение @benchmark , а @benchmark(iters=10) — это означает, что тут вызывается функция benchmark() (функция со скобками после неё обозначает вызов функции), после чего она возвращает сам декоратор.

Да, это может быть действительно сложно уместить в голове, поэтому держите правило:

Декоратор принимает функцию в качестве аргумента и возвращает функцию.

В нашем примере benchmark() не удовлетворяет этому условию, так как она не принимает функцию в качестве аргумента. В то время как функция actual_decorator() , которая возвращается benchmark() , является декоратором.

Объекты-декораторы

Напоследок стоит упомянуть, что не только функции, а любые вызываемые объекты могут быть декоратором. Экземпляры классов/объекты с методом __call__() тоже можно вызывать, поэтому их можно использовать в качестве декораторов. Эту функциональность можно использовать для создания декораторов, хранящих какое-то состояние. Например, вот декоратор для мемоизации:

Само собой, этот декоратор нужен в основном в демонстрационных целях, в реальном приложении для подобного кеширования стоит использовать functools.lru_cache .

Тут будут перечислены некоторые важные вещи, которые не были затронуты в статье или были затронуты вскользь. Вам может показаться, что они расходятся с тем, что было написано в статье до этого, но на самом деле это не так.

  • Декораторы не обязательно должны быть функциями, это может быть любой вызываемый объект.
  • Декораторы не обязаны возвращать функции, они могут возвращать что угодно. Но обычно мы хотим, чтобы декоратор вернул объект того же типа, что и декорируемый объект. Пример:
  • Также декораторы могут принимать в качестве аргументов не только функции. Здесь можно почитать об этом подробнее.
  • Необходимость в декораторах может быть неочевидной до написания библиотеки. Поэтому, если декораторы кажутся вам бесполезными, посмотрите на них с точки зрения разработчика библиотеки. Хорошим примером является декоратор представления в Flask.
  • Также стоит обратить внимание на functools.wraps() — функцию, которая помогает сделать декорируемую функцию похожей на исходную, делая такие вещи, как сохранение doctstring исходной функции.

Заключение

Надеемся, эта статья помогла вам понять, какая «магия» лежит в основе работы декораторов.

Просто о сложном — декораторы в Python

Перед тем как начать разбираться с декораторами давайте поговорим о функциях.

Что вообще делает функция? Обобщая и упрощая мы можем сказать, что функция возвращает то или иное значение в зависимости от переданных ей аргументов. Кроме этого, мы должны помнить, что в Python — все является объектами и функции не исключение. Функция в Python — это объект первого класса, это означает, что мы можем их присваивать переменным, хранить в структурах данных, передавать функцию в качестве аргументов других функций и возвращать функцию в качестве результатов работы другой функции.

Объявим три простые функции. Одна из них делает текст жирным, вторая — курсивом, третья — принимает наименование и url, а возвращает html ссылку.

Присвоим новой переменной ссылку на функцию bold и попробуем вызвать её:

Как мы видим, функция bold прекрасно отработала будучи вызванной через bold2(). При этом, Python позволяет нам обратиться к специальному свойству __name__ объекта функции и получить имя изначальной функции.

Определим функция для множественных трансформаций текста, которая получает в качестве параметров другие функции.

Как видим, функция получает три аргумента: текст для трансформации и два аргумента-функции. Функция сначала вызывает функцию переданную в параметр func1 — в нашем случае это italic(), затем наша функция вызывает функцию func2 (bold() в нашем случае) и передает ей в качестве аргумента результат работы func1. Полученный результат возвращается в основную программу. В результате мы имеем жирный и курсивный текст.

Кроме вышеописанных кейсов, функции могут быть еще и вложены в другие функции. При вызове родительской функции, вложенные функции определяются каждый раз заново, они недоступны вне контекста родительской функции. Данная концепция очень близка к декораторам, поэтому вложенность функций надо идеально понимать. Давайте определим функцию, которая внутри себя определяет две функции, одна из которых возвращает текст обрамленный в параграф, а вторая — просто текст с переносом строки через br. Родительская функция будет возвращать одну из своих внутренних функций в зависимости от переданного параметра.

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

Разберемся чуть подробнее. Первой строкой мы получаем в переменную multi_3 ссылку на функцию multi, объявленную внутри make_multiplier. При этом в make_multiplier() мы передаем число 3 и наша функция multi() “помнит” о том, что в родительском контексте n = 3 на текущий момент времени. Вторая строка — в multi_5 приходит ссылка на вновь объявленную при вызове make_multiplier() функцию multi(), которая на сей раз “помнит” о том, что в родительской контексте n = 5. После чего мы вызываем наши функции multi, передавая в них числа 2 и 10, которые будут умножены на 3 и 5 соответственно. Как разультат, вывод чисел 6 и 50.

Разобравшись с поведением функций мы готовы приступить к изучению декораторов. Что же такое декоратор? Декоратор — это функция, которая принимает в качестве аргумента другую функцию и изменяют её поведение не меняя её саму.

Мы уже делали выше вложенные функции очень похожие на декораторы, поэтому пример, который мы сейчас разберем — должен показаться знакомым и понятным. Определим функцию body, которая принимает некий контент и возвращает его обрамляя при этом тегами … . Чтобы получилась полноценная html страничка, надо обрамить результат этой работы в … теги, но что, если мы хотим сделать так, чтобы вызывая body мы сразу же получали полноценную html страничку, при этом изменять код body мы не хотим? Встречайте — на сцену выходят декораторы.

В строке body = html_page(body) как раз происходит создание “декоратора”. Мы говорим, что теперь body ссылается не на себя, а на результат выполнения функции html_page, которая, в свою очередь, возвращает функцию wrapper, принимающую в качестве аргумента некий контент и возвращающую результат выполнения функции, полученной в родительском контексте функцией html_page, но обрамленный в теги …. Если мы попробуем сейчас обратиться к свойству __name__ функции body, то увидим, что оно равно “wrapper”.

Мы знаем, что Python — это про удобный, красивый и читаемый код. Поэтому, конечно, разработчики придумали более элегантный вид объявлять декораторы для функции, чем писать, что имя функции равняется новой функции, в которую мы передаем в качестве аргумента саму нашу функцию. Это слишком сложно. Давайте перепишем наш код используя синтаксический сахар.

Как видно, для того, чтобы объявить декоратор, нам достаточно в строке, предшествующей декорируемой функции, написать конструкцию вида @ и сразу следом без пробелов наименование функции, которая будет декоратором для нашей. Удобно и красиво.

Если у вас в коде много декораторов, то хорошей практикой является вынесение всех декораторов в отдельный модуль, чтобы в последующем использовать их там где надо просто написав конструкцию вида

Если мы говорим о хорошем тоне, то есть еще один нюанс. Помните, что если обратиться сейчас к свойству __name__ функции body, то получим следующее:

Хоть это и правильно, но хорошим тоном считается оставлять функциям их собственные имена. Чтобы этого избежать, при использовании декораторов необходимо импортировать библиотеку functools и внутри функции декоратора, на строчке перед объявлением функции враппера, написать конструкцию вида @functools.wraps(func). Тогда при обращении к свойству __name__ декорируемой функции, мы получим исходное её имя, потому что декоратор @functools.wraps использует функцию functools.update_wrapper, которая как раз занимается тем, что обновляет различные служебные свойства функций, вроде __name__ или __doc__. В итоге мы получим следующий код:

Подытоживая все вышесказанное, перед тем как показать пару примеров из реальной жизни, использующих декораторы, давайте сформируем для себя “рыбу”-декоратор, который можно скопировать и подстроить под ваши нужды. Запомним, что декораторы у нас лежат в отдельном модуле decorators.py, из которого их можно импортировать в любое время, в любое место.

Обратите внимание, что в отличие от прошлых примеров, здесь мы используем “резиновые” атрибуты у функций, чтобы не быть привязанным к строгому количеству передаваемых аргументов. Таким образом, наш декоратор будет работать для любой функции. Теперь наших теоретических знаний достаточно для того, чтобы разобрать пример с использованием декораторов. Напишем декоратор, который подсчитывает количество времени затраченного на работу той или иной функции.

Итак, у нас есть две функции, которые вычисляют сумму квадратов чисел от 0 до 100 000 в первой функции и от 0 до 1 000 000 во второй функции. При этом эти вычисления повторяются определенное количество раз, которое передается в эти функции в виде атрибутов.Для обеих этих функций подходит наш декоратор my_timer, который использует библиотеку time для начала отсчета времени, затем непосредственно выполняет декорируемую функцию и, наконец, фиксирует время после выполнения функции, чтобы затем из этого времени вычесть начальное и как итог — получить время работы функции.

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

Путь к пониманию декораторов в Python

Прим. Wunder Fund: В этой статье разбираемся, что такое декораторы в Python, зачем они нужны, и в чем их прикол. Статья будет полезна начинающим разработчикам.

Материал рассчитан на начинающих программистов, которые хотят разобраться с тем, что такое декораторы, и с тем, как применять их в своих проектах.

Что такое декораторы?

Декораторы — это обёртки вокруг Python-функций (или классов), которые изменяют работу того, к чему они применяются. Декоратор максимально абстрагирует собственные механизмы. А синтаксические конструкции, используемые при применении декораторов, устроены так, чтобы они как можно меньше влияли бы на код декорируемых сущностей. Разработчики могут создавать код для решения своих специфических задач так, как привыкли, а декораторы могут использовать исключительно для расширения функционала своих разработок. Всё это — очень общие утверждения, поэтому перейдём к примерам.

В Python декораторы используются, в основном, для декорирования функций (или, соответственно, методов). Возможно, одним из самых распространённых декораторов является декоратор @property :

В последней строке кода, мы можем обратиться к члену area экземпляра класса Rectangle как к атрибуту. То есть — нам не нужно вызывать метод area . Вместо этого при обращении к area как к атрибуту (то есть — без использования скобок, () ), соответствующий метод вызывается неявным образом. Это возможно благодаря декоратору @property .

Как работают декораторы?

Размещение конструкции @property перед определением функции равносильно использованию конструкции вида area = property(area) . Другими словами, property — это функция, которая принимает другую функцию в качестве аргумента и возвращает ещё одну функцию. Именно этим и занимаются декораторы.

В результате оказывается, что декоратор меняет поведение декорируемой функции.

Декораторы функций

Декоратор retry

Мы дали довольно-таки размытое определение декораторов. Для того чтобы разобраться с тем, как именно они работают, займёмся написанием собственных декораторов.

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

В соответствии с ранее данным определением — мы можем сделать код нашего простого декоратора таким:

Наш декоратор носит имя retry . Он принимает в виде аргумента ( func ) любую функцию. Внутри декоратора определяется новая функция ( _wrapper ), после чего осуществляется возврат этой функции. Тому, кто впервые видит код декоратора, может показаться непривычным объявление одной функции внутри другой функции. Но это — совершенно корректная синтаксическая конструкция, следствием применения которой является тот полезный для нас факт, что функция _wrapper видна лишь внутри пространства имён декоратора retry .

Обратите внимание на то, что в этом примере мы декорируем функцию might_fail() с использованием конструкции, которая выглядит @retry . После имени декоратора нет круглых скобок. В результате получается, что когда мы, как обычно, вызываем функцию might_fail() , на самом деле, вызывается декоратор retry , которому передаётся, в виде первого аргумента, целевая функция ( might_fail ).

Получается, что, в общей сложности, тут мы поработали с тремя функциями:

В некоторых случаях нужно, чтобы декораторы принимали бы дополнительные аргументы. Например, нам может понадобиться, чтобы декоратор retry принимал бы число, задающее количество попыток запуска декорируемой функции. Но декоратор обязательно должен принимать декорируемую функцию в качестве первого аргумента. Не будем забывать и о том, что нам не надо вызывать декоратор при декорировании функции. То есть — о том, что перед определением функции мы используем конструкцию @retry , а не @retry() . Подытожим:

Декоратор — это всего лишь функция (которая, в качестве аргумента, принимает другую функцию).

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

Следовательно, мы можем ввести в код четвёртую функцию, которая принимает параметр, с помощью которого мы хотим настраивать поведение декоратора, и возвращает функцию, которая и является декоратором (то есть — принимает в качестве аргумента другую функцию).

Попробуем такую конструкцию:

Разберём этот код:

На первом уровне тут имеется функция retry .

Функция retry принимает произвольный аргумент (в нашем случае — max_retries ) и возвращает другую функцию — retry_decorator .

Функция retry_decorator — это и есть реальный декоратор.

Функция _wrapper работает так же, как и прежде (только теперь она руководствуется сведениями о максимальном количестве перезапусков декорированной функции).

О коде нового декоратора мне больше сказать нечего. Теперь поговорим об его использовании:

Функция might_fail теперь декорируется с помощью вызова функции вида @retry(2) .

Вызов retry(2) приводит к тому, что вызывается функция retry , которая и возвращает реальный декоратор.

В итоге функция might_fail декорируется с помощью retry_decorator , так как именно эта функция представляет собой результат вызова функции retry(2) .

Декоратор timer

Напишем ещё один полезный декоратор — timer («таймер»). Он будет измерять время выполнения декорированной с его помощью функции:

Вот результаты выполнения этого кода:

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

Декоратор functools.wraps

Анализируя вышеприведённый код, вы могли заметить, что сама функция _wrapper декорирована с помощью @functools.wraps . Но это никоим образом не меняет логику или функционал декоратора timer . При этом разработчик может принять решение о целесообразности использования functools.wraps .

Но, так как декоратор @timer может быть представлен как complex_calculation = timer(complex_calculation) , он обязательно изменяет функцию complex_calculation . В частности, он меняет некоторые из отражённых магических методов:

При использовании декоратора @functools.wraps эти атрибуты возвращаются к их исходному состоянию.

Вот что получится без @functools.wraps :

А использование @functools.wraps даёт нам следующее:

Декораторы классов

До сих пор мы обсуждали декораторы для функций. Но декорировать можно и классы.

Возьмём декоратор timer из предыдущего примера. Он отлично подходит и в качестве обёртки для класса:

Вот что нам это даст:

Видно, что здесь нет сведений о времени выполнения метода complex_calculation . Вспомним о том, что конструкция, начинающаяся с @ — это всего лишь эквивалент MyClass = timer(MyClass) . То есть — декоратор вызывается только когда «вызывают» класс. «Вызов» класса — это создание его экземпляра. Получается, что timer вызывается лишь при выполнении строки кода my_obj = MyClass() .

При декорировании класса методы этого класса не подвергаются автоматическому декорированию. Проще говоря — использование обычного декоратора для декорирования обычного класса приводит лишь к декорированию конструктора (метод __init__ ) этого класса.

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

Вот что получится:

В ходе работы этого кода происходит следующее:

Функция __init__ вызывается при декорировании some_function . Тут, снова, не забываем о том, что использование декоратора — это аналог конструкции some_function = MyDecorator(some_function) .

Функция __call__ вызывается при использовании экземпляра класса, например — при вызове функции. Функция some_function — это теперь экземпляр класса MyDecorator , но использовать мы её при этом планируем как функцию. За это отвечает магический метод __call__ , в имени которого используются два символа подчёркивания.

Декорирование класса в Python, с другой стороны, работает путём изменения класса извне (то есть — из декоратора).

Взгляните на этот код:

Вот что он выдаст:

Если вспомнить определение декоратора, то всё, что тут происходит, следует уже знакомой нам логике:

Вызов my_obj = MyClass() инициирует последовательность действий, которая начинается с вызова декоратора.

Декоратор add_calc дополняет класс методом calc .

В итоге создаётся экземпляр класса с использованием конструктора.

Декораторы можно использовать для изменения классов по принципам, соответствующим механизмам наследования. Хорошо это для некоего проекта, или плохо — сильно зависит от архитектуры конкретного Python-проекта. Декоратор dataclass из стандартной библиотеки — это отличный пример целесообразности применения декоратора, а не наследования. Скоро мы остановимся на этом подробнее.

Использование декораторов

Декораторы в стандартной библиотеке Python

В следующих разделах мы познакомимся с несколькими наиболее популярными и наиболее широко используемыми декораторами, которые включены в состав стандартной библиотеки Python.

Декоратор property

Как уже было сказано, @property — это, скорее всего, один из самых популярных Python-декораторов. Его цель заключается в том, чтобы обеспечить доступ к результатам вызова метода класса в такой форме, как будто этот метод является атрибутом. Конечно, существует и альтернатива @property , что позволяет, при выполнении операции присваивания значения, самостоятельно выполнять вызов метода.

Декоратор staticmethod

Ещё один широко известный декоратор — это @staticmethod . Он используется в ситуациях, когда надо вызвать функцию, объявленную в классе, не создавая при этом экземпляр данного класса:

Декоратор functools.cache

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

Например, можно соорудить нечто вроде такого кода:

Использование глобальной переменной, вроде _cached_result , проверка её на None , запись в эту переменную некоего значения в том случае, если она не равна None — всё это — повторяющиеся задачи. А значит — перед нами идеальная ситуация для применения декораторов. Но самостоятельно писать такой декоратор нам не придётся — в стандартной библиотеке Python есть именно то, что нужно для решения этой задачи — декоратор cache :

Теперь, при попытке вызова complex_calculations() , Python, перед вызовом функции something_complex , проверяет, имеется ли кешированный результат её работы. Если результат её вызова имеется в кеше — something_complex не придётся вызывать дважды.

Декоратор dataclass

Там, где мы говорили о декораторах классов, мы видели, что декораторы можно использовать для модификации поведения классов, применяя ту же схему, которая используется для изменении поведения классов при наследовании.

Модуль стандартной библиотеки dataclasses — это хороший пример механизма, применение которого в определённых ситуациях предпочтительнее применения механизмов наследования. Сначала давайте посмотрим на всё это в действии:

На первый взгляд кажется, что декоратор @dataclass просто снимает с нас нагрузку по написанию конструктора класса, позволяя избежать ручного написания кода, подобного следующему:

Но не всё так просто. Предположим, решено оснастить Python-проект REST-API, при этом встанет необходимость преобразовывать Python-объекты в JSON-строки.

Существует пакет dataclasses-json (не входящий в состав стандартной библиотеки), который позволяет декорировать классы данных и даёт возможность превращать объекты в их JSON-представление и выполнять обратное преобразование, производить сериализацию и десериализацию объектов.

Вот как это выглядит:

Разбирая этот код, можно сделать два наблюдения:

Декораторы могут быть вложены друг в друга. При этом важен порядок их появления в коде.

Декоратор @dataclass_json добавляет к классу метод to_dict .

Конечно, можно написать миксин (mixin, подмешанный класс), ответственный за решение всех сложных задач, связанных с типобезопасной реализацией метода to_dict . Потом можно сделать наш класс InventoryItem наследником этого класса.

В предыдущем примере, однако, декоратор оснащает класс лишь техническим функционалом (в противоположность расширению возможностей класса с учётом конкретной задачи). В результате можно отключать и подключать этот декоратор, не меняя поведения основной программы. Этот подход позволяет сохранить нашу «естественную» иерархию классов, код проекта не придётся подвергать изменениям. Декоратор dataclasses-json можно добавить в проект, не переписывая при этом тела существующих методов.

В подобном случае модификация поведения класса с помощью декораторов выглядит гораздо более элегантным решением (за счёт его лучшей модульности), чем применение наследования или миксинов.

О, а приходите к нам работать? ��

Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.

Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.

Разбираемся с декораторами в Python

Декораторы – это обертка вокруг функций (или классов) в Python, которая меняет способ работы этой функции. Декоратор абстрагирует свой собственный функционал. Нотация декоратора в целом наименее инвазивна. Разработчик может писать свой код так, как ему хочется, и использовать декораторы только для расширения функциональности. Все это звучит крайне абстрактно, поэтому давайте обратимся к примерам.

В Python декораторы используются для «декорирования» функций (или методов). Возможно, один из самых популярных декораторов – это @property :

Как видно в последней строке, вы можете получить доступ к area нашего Rectangle , как к атрибуту, то есть вам не нужно вызывать метод area . Вместо этого при доступе к area , как к атрибуту (без () ), метод вызывается неявно из-за декоратора @property

Как это работает?

Написать @property перед определением функции – то же самое, что написать

area = property(area) . Другими словами: property – это функция, которая принимает другую функцию в качестве аргумента и возвращает третью. Так и ведут себя декораторы.

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

Пишем свои декораторы

Декоратор retry

Давайте по этому расплывчатому определению напишем свои декораторы, чтобы понять, как они работают.

Допустим, у нас есть функция, выполнение которой мы хотим повторить, если она завершится неудачно. Нам нужна функция (декоратор), которая вызовет нашу функцию один или два раза (в зависимости от того, как функция завершится в первый раз).

С учетом нашего изначального определения декоратора мы можем написать простой декоратор следующим образом:

Retry – имя нашего декоратора, который принимает в качестве аргумента любую функцию ( func ). Внутри декоратора определяется и возвращается новая функция ( _wrapper ). На первый взгляд определение одной функции внутри другой может показаться несколько непривычным. Однако синтаксически это совершенно нормально и имеет определенное преимущество, ведь функция _wrapper существует только внутри пространства имен нашего декоратора retry .

Обратите внимание, что в примере мы отдекорировали нашу функцию только с помощью @retry . После декоратора @retry нет круглых скобок. Таким образом, при вызове функции might_fail() декоратор @retry вызовется с нашей функцией ( might_fail ) в качестве первого аргумента.

В итоге мы обрабатываем три функции:

Иногда нужно, чтобы декоратор принимал аргументы. В нашем случае мы можем сделать параметром количество повторных попыток. Однако декоратор должен принять нашу функцию в качестве первого аргумента. Вспомните, нам не нужно было вызывать декоратор при декорировании функции с его помощью, то есть мы просто писали @retry , а не @retry() .

Декоратор – не что иное, как функция (которая принимает другую функцию в качестве аргумента)

Чтобы использовать декоратор, нужно поместить его перед определением функции без ее вызова

Следовательно мы могли бы написать четвертую функцию, которая принимает нужный нам параметр в качестве конфигурации и возвращает функцию, которая на самом деле и есть декоратор (который принимает другую функцию в качестве аргумента).

Давайте попробуем так:

Разложим на составляющие:

Сначала у нас была функция retry ;

Retry принимает произвольный аргумент ( max_retries в нашем случае) и возвращает функцию;

retry_decorator — это функция, возвращаемая retry и по факту наш декоратор;

_wrapper работает так же, как и раньше (теперь он просто выполняет максимальное количество попыток).

Для определения нашего декоратора:

На этот раз might_fail декорируется вызовом функции, т.е. @retry(2) ;

retry(2) вызывает retry , а та возвращает сам декоратор;

might_fail в конечном итоге будет отдекорирована retry_decorator , поскольку эта функция является результатом вызова retry(2) .

Декоратор-таймер

Вот еще один пример полезного декоратор. Давайте напишем декоратор, который будет возвращать время выполнения функций.

Как мы видим, декоратор-таймер выполняет код до и после функции и работает точно так же, как и в последнем примере.

functools.wraps

Возможно, вы заметили, что сама функция _wrapper отдекорирована @functools.wraps . Этот факт никак не меняет логику или функциональность нашего декоратора-таймера. С таким же успехом вы могли бы вообще не использовать @functools.wraps .

Однако, поскольку наш декоратор @timer мог быть написан как: complex_calculation = timer(complex_calculation) , декоратор обязательно изменит нашу функцию complex_calculation . В частности, он меняет некоторые атрибуты специальных методов:

При использовании @functools.wraps все эти атрибуты возвращаются к значениям по умолчанию.

Без @functools.wraps :

С @functools.wraps :

Декораторы классов

До настоящего момента мы рассматривали декораторы функций. Однако их можно также использовать для классов.

Давайте возьмем наш таймер из примера выше. Мы совершенно спокойно сможем обернуть в него класс:

Очевидно, что выполнение complex_calculation не заняло времени. Помните, что @нотация — это просто эквивалент записи MyClass = timer(MyClass) , т.е. декоратор будет вызван только тогда, когда вы «вызовете» класс. Вызов класса означает создание его экземпляра, поэтому таймер отработает только для строки my_obj = MyClass() .

Методы класса не декорируются автоматически при декорации класса. Проще говоря, при использовании декоратора для обычного класса декорируется только его конструктор (метод __init__ ).

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

Как это работает:

__init__ вызывается при оформлении some_function . Опять же, помните, что декорирование — это то же самое, что some_function = MyDecorator(some_function) .

__call__ вызывается, когда используется экземпляр класса, например, при вызове функции. Поскольку some_function теперь является экземпляром моего декоратора, но мы все еще хотим использовать ее как функцию, нам понадобится специальный метод __call__ .

Декорирование класса в Python работает как изменение класса извне (т.е. из декоратора).

Опять же, вспомним определение декоратора, ведь все, что здесь происходит, следует той же логике:

my_obj = MyClass() сначала вызывает декоратор,

Декоратор add_calc добавляет метод calc к классу

В итоге класс создается с помощью конструктора.

Вы можете использовать декораторы для изменения классов подобно наследованию. Хорошо это или плохо в значительной степени зависит от архитектуры вашего проекта в целом. Декоратор dataclass из стандартной библиотеки — отличный пример разумного использования, при котором декораторы предпочтительнее наследования. Мы сейчас поговорим об этом.

Использование декораторов

Декораторы в стандартной библиотеке Python

В следующих разделах мы познакомимся с некоторыми популярными и полезными декораторами, которые уже есть в стандартной библиотеке.

Property

Как мы уже знаем, декоратор @property , вероятно, один из наиболее часто используемых декораторов в Python. Он нужен, чтобы вы могли получить доступ к результату выполнения метода как к атрибуту. Конечно, существует аналог @property , с помощью которого вы можете вызывать метод под капотом при выполнении операции присваивания.

Staticmethod

Еще один популярный декоратор – staticmethod . Он нужен, если вы хотите вызвать функцию, определенную внутри класса не создавая экземпляр класса:

functools.cache

Когда вы имеете дело с функциями, которые выполняют сложные вычисления, вам может понадобиться закэшировать их результат.

Можно сделать что-то вроде этого:

Сохранение глобальной переменной, такой как _cached_result , проверка ее на None и помещение результата в эту переменную являются повторяющимися задачами. Все это делает их идеальным кандидатом на должность декоратора. К счастью, в стандартной библиотеке Python есть декоратор, который сделает это все за нас:

Теперь каждый раз, когда вы вызываете complex_calculations() , Python сначала проверяет наличие кэшированного результата, прежде чем вызывать something_complex . Если в кэше есть результат, something_complex не будет вызываться дважды.

Dataclasses

В разделе про декораторы классов мы видели, что их можно использовать для изменения поведения классов по аналогии с наследованием.

Модуль dataclasses в стандартной библиотеке является хорошим примером, когда использование декоратора предпочтительнее, чем использование наследования. Давайте сначала посмотрим, как использовать dataclasses :

На первый взгляд, декоратор @dataclass добавил только конструктор, поэтому мы избегали шаблонного кода, подобного этому:

Однако, если вы решите написать REST-API для своего проекта на Python и вам нужно преобразовать ваши объекты Python в строки JSON, для есть пакет под названием dataclasses-json (отсутствует в стандартной библиотеке), который декорирует классы и обеспечивает сериализацию и десериализацию объектов в строки JSON и наоборот.

Давайте посмотрим, как это выглядит:

Отсюда можно сделать два вывода:

Декораторы могут быть вложенными. Важен порядок их написания.

Декоратор @dataclass_json добавил в наш класс метод to_dict

Конечно, мы могли бы написать класс-примесь, который выполняет тяжелую работу по реализации метода to_dict , безопасный для типов данных, а затем наследовать класс InventoryItem от него.

Однако в данном случае декоратор добавляет только техническую функциональность (в отличие от расширения функционала). В результате мы можем просто «включать и выключать» декоратор без изменения поведения нашего приложения. «Естественная» иерархия классов сохраняется, и никаких изменений в код вносить не нужно. Мы можем добавить декоратор dataclasses-json в проект без изменения уже готовых методов.

В таком случае изменение класса с помощью декоратора выглядит элегантнее (потому что оно сохраняет модульность), чем наследование или использование примесей.

Завтра в OTUS состоится открытое занятие «Docker для Python разработчика». Рассмотрим best practices написания Dockerfile’ов и работы с docker’ом в целом. Обсудим нюансы как общего характера, так и Python-специфичные. Регистрация — здесь.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *