Прим. Wunder Fund: В этой статье разбираемся, что такое декораторы в Python, зачем они нужны, и в чем их прикол. Статья будет полезна начинающим разработчикам. Материал рассчитан на начинающих программистов, которые хотят разобраться с тем, что такое декораторы, и с тем, как применять их в своих проектах. Декораторы — это обёртки вокруг Python-функций (или классов), которые изменяют работу того, к чему они применяются. Декоратор максимально абстрагирует собственные механизмы. А синтаксические конструкции, используемые при применении декораторов, устроены так, чтобы они как можно меньше влияли бы на код декорируемых сущностей. Разработчики могут создавать код для решения своих специфических задач так, как привыкли, а декораторы могут использовать исключительно для расширения функционала своих разработок. Всё это — очень общие утверждения, поэтому перейдём к примерам. В Python декораторы используются, в основном, для декорирования функций (или, соответственно, методов). Возможно, одним из самых распространённых декораторов является декоратор @property: В последней строке кода, мы можем обратиться к члену area экземпляра класса Rectangle как к атрибуту. То есть — нам не нужно вызывать метод area. Вместо этого при обращении к area как к атрибуту (то есть — без использования скобок, ()), соответствующий метод вызывается неявным образом. Это возможно благодаря декоратору @property. Размещение конструкции @property перед определением функции равносильно использованию конструкции вида area = property(area). Другими словами, property — это функция, которая принимает другую функцию в качестве аргумента и возвращает ещё одну функцию. Именно этим и занимаются декораторы. В результате оказывается, что декоратор меняет поведение декорируемой функции. Мы дали довольно-таки размытое определение декораторов. Для того чтобы разобраться с тем, как именно они работают, займёмся написанием собственных декораторов. Предположим, имеется функция, которую мы хотим запустить повторно в том случае, если при её первом запуске произойдёт сбой. То есть — нам нужна функция (декоратор, имя которого, retry, можно перевести как «повтор»), которая вызывает нашу функцию один или два раза (это зависит от того, возникнет ли ошибка при первом вызове функции). В соответствии с ранее данным определением — мы можем сделать код нашего простого декоратора таким: Наш декоратор носит имя retry. Он принимает в виде аргумента (func) любую функцию. Внутри декоратора определяется новая функция (_wrapper), после чего осуществляется возврат этой функции. Тому, кто впервые видит код декоратора, может показаться непривычным объявление одной функции внутри другой функции. Но это — совершенно корректная синтаксическая конструкция, следствием применения которой является тот полезный для нас факт, что функция _wrapper видна лишь внутри пространства имён декоратора retry. Обратите внимание на то, что в этом примере мы декорируем функцию might_fail() с использованием конструкции, которая выглядит @retry. После имени декоратора нет круглых скобок. В результате получается, что когда мы, как обычно, вызываем функцию might_fail(), на самом деле, вызывается декоратор retry, которому передаётся, в виде первого аргумента, целевая функция (might_fail). Получается, что, в общей сложности, тут мы поработали с тремя функциями: retry _wrapper 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 выполняет какой-то код до и после вызова декорируемой функции. В остальном же он работает точно так же, как декоратор, рассмотренный в предыдущем разделе. Но при его написании мы воспользовались и кое-чем новым. Анализируя вышеприведённый код, вы могли заметить, что сама функция _wrapper декорирована с помощью @functools.wraps. Но это никоим образом не меняет логику или функционал декоратора timer. При этом разработчик может принять решение о целесообразности использования functools.wraps. Но, так как декоратор @timer может быть представлен как complex_calculation = timer(complex_calculation), он обязательно изменяет функцию complex_calculation. В частности, он меняет некоторые из отражённых магических методов: __module__ __name__ __qualname__ __doc__ __annotations__ При использовании декоратора @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. Как уже было сказано, @property — это, скорее всего, один из самых популярных Python-декораторов. Его цель заключается в том, чтобы обеспечить доступ к результатам вызова метода класса в такой форме, как будто этот метод является атрибутом. Конечно, существует и альтернатива @property, что позволяет, при выполнении операции присваивания значения, самостоятельно выполнять вызов метода. Ещё один широко известный декоратор — это @staticmethod. Он используется в ситуациях, когда надо вызвать функцию, объявленную в классе, не создавая при этом экземпляр данного класса: При работе с функциями, выполняющими сложные вычисления, может понадобиться кешировать результаты их работы. Например, можно соорудить нечто вроде такого кода: Использование глобальной переменной, вроде _cached_result, проверка её на None, запись в эту переменную некоего значения в том случае, если она не равна None — всё это — повторяющиеся задачи. А значит — перед нами идеальная ситуация для применения декораторов. Но самостоятельно писать такой декоратор нам не придётся — в стандартной библиотеке Python есть именно то, что нужно для решения этой задачи — декоратор cache: Теперь, при попытке вызова complex_calculations(), Python, перед вызовом функции something_complex, проверяет, имеется ли кешированный результат её работы. Если результат её вызова имеется в кеше — something_complex не придётся вызывать дважды. Там, где мы говорили о декораторах классов, мы видели, что декораторы можно использовать для модификации поведения классов, применяя ту же схему, которая используется для изменении поведения классов при наследовании. Модуль стандартной библиотеки 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 разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь. Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров. Присоединяйтесь к нашей команде.