Паттерн ДЕКОРАТОР
Паттерн Декоратор динамически наделяет объект новыми возможностями и является гибкой альтернативой наследованию (субклассированию) в области расширения функциональности.
- Сomponent — абстрактный класс который наследуется ConcreteComponent.
- ConcreteComponent — объект поведение которого собираемся динамически расширять.
- Decorator реализует тот же интерфейс или абстрактный класс (в нашем случае абстрактный класс), что и декорируемый компонент).
- ConcreteDecoratorA — содержит переменную экземпляра в которой хранится декорируемый объект (Component)
- СoncreteDecoratorB — показано что декораторы могут расширять состояние компонента, если нужно.
Декораторы могут добавлять новые методы, однако новое поведение обычно добавляется до или после вызова существующего метода компонента.
Теперь попробуем использовать данный паттерн практически, допустим (навеяно примером книги) у нас есть некий набор напитков Чай, Кофе имеющих стоимость, и есть набор добавок к напиткам Молоко, Сахар которые тоже имеют стоимость. Готовый продукт подаваемый клиенту содержит напиток с добавками (или без) и в конечном итоге стоимость равна сумме стоимости всех ингридиентов.
Допустим мы реализовали класс Напитка, который умеет возвращать его стоимость. Если мы реализуем класс добавку как класс — декоратор, мы сможем “обертывать” обьект напитка и добавлять стоимость добавки к стоимости напитка.
Схематично это может выглядеть как то так, у нас есть объект Чай который декорирован объектом Добавка Сахар. Т.е. на этапе когда я создаю экземпляр класса добавки — Сахар, экземпляр класса напитка Чай у меня уже есть. Когда я создаю экземпляр декоратора-добавки Сахар я передаю ему уже готовый экземпляр напитка Чай. Цена готового напитка рассчитывается как цена Сахар + Чай, соответственно фактически экземпляр класса Сахар при вызове метода возвращающего цену должен вызвать метод возвращающий цену обернутого объекта (Чай) и потом добавить свою цену и вернуть общую стоимость.
На самом деле вложенность может быть сколь угодно уровневой, допустим мы можем обернуть предыдущий пример еще в один тип добавки Молоко.
Тогда общая цена напитка будет состоять из “цена Чая” + “цена Сахара” + “цена Молока”. Т.е. при вызове метода расчета стоимости у экземпляра добавки Молоко ( который обертывает экземпляр Сахар, который в свою очередь содержит обернутый экземпляр Чай), вызовется метод расчета стоимости обернутого обьекта (Сахар), в свою очередь метод расчета стоимости экземпляра Сахар вызовет метод обернутого объекта Чай, добавит свою стоимость (Сахара) и вернет значение экземпляру Молоко. Экземпляр Молока добавит свои стоимость к возвращенному (Чай+Сахар) и вернет общую стоимость напитка. Немного запутанно, но на самом деле не очень сложно.
Такие декорации можно комбинировать, очень гибко без изменения кода. Допустим можно сделать Чай с двойным Молоком, нужно лишь два раза обернуть экземпляр класса Чай декоратором Молоко.
Диаграмма классов нашего примера будет выглядеть так:
AbstractBeverage абстрактный класс напитка в котором мы определим методы cost() и getDescription(). cost() это абстрактный метод который должен быть реализован в классах реальных напитков. Метод getDescription() метод который наследуется классами настоящих напитков и можно будет использовать для получения названия напитка.
GreenTea, BlackCoffee классы реализующие сущности конкретных напитков Кофе и Чай. Методы cost() в них возвращают конкретную стоимость.
AbstractCondiments — абстрактный класс наследованный от AbstractBeverage, в нем мы переопределили getDescription() как абстратный, я хочу чтоб конкретные классы добавок обязаны были иметь свои собственные реализации getDescription(). Метод cost() также должен быть определен в конкретном классе добавки, это поведение определено в родительском классе AbstractBeverage.
Sugar, Milk — конкретные классы реализующие AbstractCondiments. В конструктор классов я передаю экземпляр конкретного напитка и запоминаю его в переменной self.decorated. Таким образом в каждом экземпляре добавки я запоминаю обьект который в конечном итоге является производным от AbstractBeverage, так как мы все наследуем от него. self.decorated может содержать как конкретный экземпляр напитка, так и обернутую конструкцию добавка(напиток), при чем глубина вложений не важна, мы оперируем на уровне абстракции у которой в конечном итоге знаем, что можно вызвать cost() и getDescription().
Прелесть в том, что мы программируем не привязываясь к конкретным типам.
Реализация у меня получилась похожая на нечто вот такое:
Теперь попробуем “приготовить” Чай без добавок:
А теперь приготовим кофе с молоком и сахаром: — создадим экземпляр BlackCoffee
- добавим сахара — создавая экземпляр Sugar — передадим созданный экземпляр BlackCofee и таким образом экземпляр Sugar запомнит экземпляр BlackCofee в переменной self.decorated. Мы таким образом декорируем (обертываем) экземпляр BlackCofee :
- добавим аналогично молоко, теперь coffee это уже не просто чистый кофе а экземпляр Sugar содержащий в себе ссылку на чистый кофе экземпляр BlackCoffee , созданный на первом шаге.
- теперь переменная coffee содержит ссылку на конструкцию из чего то, что схематично можно представить как Milk(Sugar(BlackCoffee)) Можно получить стоимость полученного напитка:
- и правильно в общем получается если сложить цены ингридиентов:
- цена кофе = 0,50
- цена сахара = 0,30
- цена молока = 0,25
0,50 + 0,30 + 0,25 = 1,05
теперь можно обернуть еще раз Milk и увидим что стоимость увеличится:
Таким образом можно вкладывать экземпляры и метод cost() будет вызываться по цепочке, в конечном итоге мы получим общую стоимость.
Это происходит потому что мы вызываем метод cost() у чего то обернутого self.decorated и в свою очередь если у “чего то обернутого” в свою очередь содержится обернутый объект у этого объекта также вызовется метод cost(). Так будет происходить пока не дойдет очередь до обьекта у которого нет “чего то обернутого” и у него вызовется метод cost(), он вернет значение вызвавшему его экземпляру, тот добавит свою стоимость и вернет выше, т.е цепочка пойдет в обратном направлении.
Вывод
Наследование одна из форм расширения, но оно не всегда обеспечивает гибкость архитектуры.
Следует предесмотреть возможность расширения без изменения существующего кода.
Композиция и делегирование часто используются для динамического добавления нового поведения (в нашем случае мы делегируем вызов cost() обьекту decorated).
Типы декораторов соответствуют типам декорируемых компонентов (соответстве может быть достигнуто посредством наследования или реализации интерфейса)
Компонент может декорироваться любым количеством декораторов.
Декораторы изменяют поведение декорируемых компонентов, добавляя новую функциональность до или после или даже вместо вызовов методов компонентов.
Related Posts:
Comments
About Alexander Kamyanskiy
I love Python and happy to use it everyday for develop Web applications, writing tests and others cool things.
Декораторы в JavaScript с нуля
Декоратор — это средство, которое позволяет обернуть одну функцию другой и расширить ее возможности. Вы «декорируете» существующий код, обернув его другим кодом. Этот прием известен всем, кто знаком с композицией функций или функциями высшего порядка.
Декораторы — явление не новое. Они используются и в других языках, например в Python, и даже в функциональном программировании на JavaScript. Но об этом мы поговорим позже.
Зачем нужны декораторы?
Они позволяют писать более чистый код, придерживаясь концепции композиции, и распространять единожды разработанную возможность на несколько функций и классов. Используя декораторы, вы сможете писать код, который проще отлаживать и сопровождать.
С декораторами код основной функции становится компактным, поскольку весь код, предназначенный для расширения ее возможностей, пишется за ее пределами. За счет декораторов можно добавлять в код новые возможности, не усложняя его.
Сейчас предложение к стандарту о декораторах классов находится на 2-м этапе рассмотрения, и к этому предложению еще может добавиться множество полезных дополнений.
Совет. Делитесь компонентами многоразового использования для разных проектов на платформе Bit (Github). Это простой способ документировать и систематизировать независимые компоненты из любых проектов и делиться ими.
Платформа открывает широкие возможности повторного использования кода, совместной работы над независимыми компонентами и разработки масштабируемых приложений.
Bit поддерживает Node, TypeScript, React, Vue, Angular и другие фреймворки JS.
Примеры React-компонентов многоразового использования на Bit.dev
Декораторы функций
Что такое декораторы функций?
Декораторы функций — это такие же функции. Они принимают функцию в качестве аргумента и возвращают другую функцию, которая расширяет поведение функции-аргумента. Новая функция не изменяет функцию-аргумент, но использует ее в своем теле. Как я уже говорил, это во многом напоминает функции высшего порядка.
Как работают декораторы функций?
Проверка аргументов — обычная практика в программировании. В таких языках, как Java, если функция ожидает два аргумента, а получает три, генерируется исключение. Но в JavaScript ошибки не будет, поскольку лишние параметры попросту игнорируются. Такое поведение функций иногда раздражает, но может быть и полезным.
Для того чтобы убедиться в допустимости аргументов, нужно проверить их на входе. Это простая операция, в которой проверяется, что у каждого параметра надлежащий тип данных, а их количество не превышает ожидаемого функцией.
Однако повторение одной и той же операции для нескольких функций может привести к повторению кода, поэтому для проверки аргументов лучше написать декоратор, который затем можно будет многократно использовать с любыми функциями.
В этом примере мы используем функцию-декоратор allArgsValid , которая принимает в качестве аргумента функцию. Декоратор возвращает другую функцию, которая обертывает функцию-аргумент. При этом функция-аргумент вызывается только в том случае, когда передаваемые в нее аргументы являются целыми числами. Иначе генерируется ошибка. Декоратор также проверяет количество передаваемых параметров: оно должно строго совпадать с количеством, которое ожидает функция.
Затем мы объявляем переменную multiply и в качестве значения присваиваем ей функцию, которая перемножает два числа. Мы передаем эту функцию умножения в функцию-декоратор allArgsValid , которая, как мы уже знаем, возвращает другую функцию. Возвращаемая функция снова присваивается переменной multiply . Таким образом, разработанный функционал можно будет без труда использовать повторно.
Декораторы классов: предложение к стандарту, рассматриваемое комитетом TC39
В функциональном программировании на JavaScript декораторы функций используются уже давно. Предложение о декораторах классов находится на 2-м этапе рассмотрения.
Классы в JavaScript — на самом деле не классы. Синтаксис классов — это всего лишь синтаксический сахар для прототипов, который упрощает работу с ними.
Напрашивается вывод, что классы — это просто функции. Тогда почему бы нам не использовать декораторы функций в классах? Давайте попробуем.
Рассмотрим на примере, как можно реализовать этот подход.
Ошибка возникает потому, что при вызове метода getBook фактически вызывается анонимная функция, возвращаемая функцией-декоратором log . Внутри анонимной функции вызывается метод obj.getBook . Но ключевое слово this внутри анонимной функции ссылается на глобальный объект, а не на объект Book . Возникает ошибка TypeError .
Это можно исправить, передав экземпляр объекта Book в метод getBook .
Нам также нужно передать объект Book в функцию-декоратор log , чтобы затем можно было передать его в метод obj.getBook , используя this .
Это решение прекрасно работает, но нам пришлось идти обходными путями. В новом предложении синтаксис оптимизирован, что упрощает реализацию подобных решений.
Примечание. Для выполнения кода из приведенных ниже примеров можно использовать Babel. JSFiddle — более простая альтернатива, которая позволяет опробовать эти примеры в браузере. Предложения еще не дошли до последнего этапа рассмотрения, поэтому использовать их в продакшене не рекомендуется: их функционирование пока не идеально и в синтаксис могут внести изменения.
Декораторы классов
В новых декораторах используется специальный синтаксис с префиксом @. Для вызова функции-декоратора log будем использовать такой синтаксис:
В предложении в функции-декораторы внесли некоторые изменения по сравнению со стандартом. Когда функция-декоратор применяется к классу, она получает только один аргумент. Это аргумент target , который по сути является объектом декорируемого класса.
Имея доступ к аргументу target , вы можете внести в класс необходимые изменения. Можно изменить конструктор класса, добавить новые прототипы и т. д.
Рассмотрим пример, в котором используется класс Book , мы с ним уже знакомы.
Как видите, декоратор log получает аргумент target и возвращает анонимную функцию. Она выполняет инструкцию log , а затем создает и возвращает новый экземпляр target , который является классом Book . Можно добавить к target прототипы с помощью target.prototype.property.
Более того, с классом могут использоваться несколько функций-декораторов, как показано в этом примере:
Декораторы свойств класса
В их синтаксисе, как и в синтаксисе декораторов классов, используется префикс @ . В декораторы свойств класса можно передавать параметры точно так же, как в другие декораторы, которые мы рассмотрели на примерах.
Декораторы методов класса
Аргументы, которые передаются в декоратор метода класса, будут отличаться от аргументов декоратора класса. Декоратор метода класса получает не один, а три параметра:
target — объект, в котором содержатся конструктор и методы, объявленные внутри класса;
name — имя метода, для которого вызывается декоратор;
descriptor — объект дескриптора, соответствующий методу, для которого вызывается декоратор. О дескрипторах свойств можно подробнее почитать здесь.
Большинство манипуляций будет выполняться с аргументом descriptor. При использовании с методом класса объект дескриптора имеет 4 атрибута:
configurable — логическое значение, которое определяет, можно ли изменять свойства дескриптора;
enumerable — логическое значение, которое определяет, будет ли свойство видимым при перечислении свойств объекта;
value — значение свойства. В нашем случае это функция;
writable — логическое значение, которое определяет, возможна ли перезапись свойства.
Рассмотрим пример с классом Book .
В нем используется функция-декоратор readOnly , которая делает метод getBook в классе Book доступным только для чтения. С этой целью для свойства дескриптора writable устанавливается значение false . По умолчанию для него установлено значение true .
Если значение writable не изменить, свойство getBook можно будет перезаписать, например, так:
Декораторы поля класса
Декораторы могут использоваться и с полями классов. Хотя TypeScript поддерживает поля классов, предложение добавить их в JavaScript пока находится на 3-м этапе рассмотрения.
В функцию-декоратор, используемую с полем класса, передаются те же аргументы, которые передаются при использовании декоратора с методом класса. Разница заключается лишь в объекте дескриптора. В отличие от использования декораторов с методами классов, при использовании с полями классов объект дескриптора не содержит атрибута value . Вместо него в качестве атрибута используется функция initializer . Поскольку предложение по добавлению полей классов пока находится на стадии рассмотрения, о функции initializer можно почитать в документации. Функция initializer вернет начальное значение переменной поля класса.
Если полю значение не присвоено ( undefined ), атрибут writable объекта дескриптора использоваться не будет.
Рассмотрим пример. Будем работать с уже знакомым нам классом Book .
В этом примере значение свойства id переводится в верхний регистр. Функция-декоратор upperCase проверяет наличие функции initializer , чтобы гарантировать, что значению поля присвоено значение (то есть значение не является undefined ). Затем она проверяет, является ли присвоенное значение «условно истинным» (прим. пер.: англ. truthy — значение, превращающееся в true при приведении к типу Boolean ), и затем переводит его в верхний регистр. При вызове метода getId значение будет выведено в верхнем регистре. При использовании декораторов с полями классов можно передавать параметры точно так же, как и в других случаях.
Варианты использования
Вариантов использования декораторов бесконечно много. Посмотрим, как программисты реализуют их в реальных приложениях.
Декораторы в Angular
Если вы знакомы с TypeScript и Angular, вы наверняка сталкивались с использованием декораторов в классах Angular, например @Component , @NgModule , @Injectable , @Pipe и т. д. Это встроенные декораторы классов.
Декораторы в MobX широко использовались вплоть до 6-й версии. Среди них @observable , @computed и @action . Но сейчас в MobX использование декораторов не приветствуется, поскольку предложение к стандарту еще не принято. В документации говорится:
«В настоящее время декораторы не являются стандартом ES, а процесс стандартизации длится долго. Скорее всего, предусмотренное стандартом использование декораторов будет отличаться от текущего».
Библиотека Core Decorators
Это библиотека JavaScript, в которой собраны готовые к использованию декораторы. Хотя она основана на предложении о декораторах этапа 0, ее автор планирует обновление, когда предложение перейдет на 3-й этап.
В библиотеке есть такие декораторы, как @readonly , @time , @deprecate и др. С другими декораторами можно ознакомиться здесь.
Библиотека Redux для React
В библиотеке Redux для React есть метод connect , с помощью которого можно подключить компонент React к хранилищу Redux. Библиотека позволяет использовать метод connect также в качестве декоратора.
В ответе пользователя Felix Kling на Stack Overflow можно найти некоторые пояснения.
Хотя connect поддерживает синтаксис декоратора, в настоящее время команда Redux не приветствует его использование. Связано это в основном с тем, что предложение о декораторах находится на 2-м этапе рассмотрения, а значит, в него могут быть внесены изменения.
Декораторы — это мощный инструмент, который позволяет писать очень гибкий код. Наверняка вы будете часто сталкиваться с ними уже в ближайшем будущем.
Всё, что нужно знать о декораторах Python
В этом руководстве по декораторам мы рассмотрим, что они собой представляют, как их создавать и использовать. По определению, декоратор – это функция, которая принимает другую функцию и расширяет поведение последней, не изменяя ее явным образом. В этом туториале мы постараемся понять, что это значит и как реализуется.
Статья является сокращенным переводом публикации Гейра Арне Хьелле 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. Несколько примеров из реального мира
Посмотрим на несколько полезных примеров декораторов. Как вы заметите, схема применения декораторов будет соответствовать тому паттерну, что мы получили в результате наших рассуждений:
Этот блок кода является хорошим шаблоном для создания более сложных декораторов.
3.1. Декоратор для тайминга кода ⌚
Начнем с создания декоратора @timer . Он будет измерять время выполнения функции и выводить результат в консоль:
Декоратор сохраняет текущее время в переменной start_time непосредственно перед запуском декорируемой функции. Это значение впоследствии вычитается из текущего значения end_time после выполнения функции. Полученная разность run_time передается в форматированную строку. Пара примеров:
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. Декорирование классов ️
Есть два способа применения декораторов к классам. Первый способ похож на то, что мы делали с функциями, – декорировать методы класса.
Давайте определим класс, в котором декорируем некоторые из методов с помощью вышеописанных декораторов @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:
Паттерн Декоратор (Decorator)
Декоратор – это структурный паттерн проектирования, который позволяет динамически добавлять объектам новую функциональность, оборачивая их в полезные «обёртки».
Проблема
Вы работаете над библиотекой оповещений, которую можно подключать к разнообразным программам, чтобы получать уведомления о важных событиях.
Основой библиотеки является класс Notifier с методом send , который принимает на вход строку-сообщение и высылает её всем администраторам по электронной почте. Сторонняя программа должна создать и настроить этот объект, указав кому отправлять оповещения, а затем использовать его каждый раз, когда что-то случается.
Сторонние программы используют главный класс оповещений
В какой-то момент стало понятно, что одних email-оповещений пользователям мало. Некоторые из них хотели бы получать извещения о критических проблемах через SMS. Другие хотели бы получать их в виде сообщений Facebook. Корпоративные пользователи хотели бы видеть сообщения в Slack.
Каждый тип оповещения живёт в собственном подклассе
Сначала вы добавили каждый из этих типов оповещений в программу, унаследовав их от базового класса Notifier . Теперь пользователь выбирал один из типов оповещений, который и использовался в дальнейшем.
Но затем кто-то резонно спросил, почему нельзя выбрать несколько типов оповещений сразу? Ведь если вдруг в вашем доме начался пожар, вы бы хотели получить оповещения по всем каналам, не так ли?
Вы попытались реализовать все возможные комбинации подклассов оповещений. Но после того как вы добавили первый десяток классов, стало ясно, что такой подход невероятно раздувает код программы.
Комбинаторный взрыв подклассов при совмещении типов оповещений
Итак, нужен какой-то другой способ комбинирования поведения объектов, который не приводит к взрыву количества подклассов.
Решение
Наследование – это первое, что приходит в голову многим программистам, когда нужно расширить какое-то существующее поведение. Но механизм наследования имеет несколько досадных проблем.
- Он статичен. Вы не можете изменить поведение существующего объекта. Для этого вам надо создать новый объект, выбрав другой подкласс.
- Он не разрешает наследовать поведение нескольких классов одновременно. Из-за этого вам приходится создавать множество подклассов-комбинаций для получения совмещённого поведения.
Одним из способов обойти эти проблемы является замена наследования агрегацией либо композицией . Это когда один объект содержит ссылку на другой и делегирует ему работу, вместо того чтобы самому наследовать его поведение. Как раз на этом принципе построен паттерн Декоратор.
Наследование против Агрегации
Декоратор имеет альтернативное название – обёртка. Оно более точно описывает суть паттерна: вы помещаете целевой объект в другой объект-обёртку, который запускает базовое поведение объекта, а затем добавляет к результату что-то своё.
Оба объекта имеют общий интерфейс, поэтому для пользователя нет никакой разницы, с каким объектом работать – чистым или обёрнутым. Вы можете использовать несколько разных обёрток одновременно – результат будет иметь объединённое поведение всех обёрток сразу.
В примере с оповещениями мы оставим в базовом классе простую отправку по электронной почте, а расширенные способы отправки сделаем декораторами.
Расширенные способы оповещения становятся декораторами
Сторонняя программа, выступающая клиентом, во время первичной настройки будет заворачивать объект оповещений в те обёртки, которые соответствуют желаемому способу оповещения.
Программа может составлять составные объекты из декораторов
Последняя обёртка в списке и будет тем объектом, с которым клиент будет работать в остальное время. Для остального клиентского кода, по сути, ничего не изменится, ведь все обёртки имеют точно такой же интерфейс, что и базовый класс оповещений.
Таким же образом можно изменять не только способ доставки оповещений, но и форматирование, список адресатов и так далее. К тому же клиент может «дообернуть» объект любыми другими обёртками, когда ему захочется.
Аналогия из жизни
Одежду можно надевать слоями, получая комбинированный эффект
Любая одежда – это аналог Декоратора. Применяя Декоратор, вы не меняете первоначальный класс и не создаёте дочерних классов. Так и с одеждой – надевая свитер, вы не перестаёте быть собой, но получаете новое свойство – защиту от холода. Вы можете пойти дальше и надеть сверху ещё один декоратор – плащ, чтобы защититься и от дождя.
Структура
- Компонент задаёт общий интерфейс обёрток и оборачиваемых объектов.
- Конкретный компонент определяет класс оборачиваемых объектов. Он содержит какое-то базовое поведение, которое потом изменяют декораторы.
- Базовый декоратор хранит ссылку на вложенный объект-компонент. Им может быть как конкретный компонент, так и один из конкретных декораторов. Базовый декоратор делегирует все свои операции вложенному объекту. Дополнительное поведение будет жить в конкретных декораторах.
- Конкретные декораторы – это различные вариации декораторов, которые содержат добавочное поведение. Оно выполняется до или после вызова аналогичного поведения обёрнутого объекта.
- Клиент может оборачивать простые компоненты и декораторы в другие декораторы, работая со всеми объектами через общий интерфейс компонентов.
Псевдокод
В этом примере Декоратор защищает финансовые данные дополнительными уровнями безопасности прозрачно для кода, который их использует.
Пример шифрования и компрессии данных с помощью обёрток
Приложение оборачивает класс данных в шифрующую и сжимающую обёртки, которые при чтении выдают оригинальные данные, а при записи – зашифрованные и сжатые.
Декораторы, как и сам класс данных, имеют общий интерфейс. Поэтому клиентскому коду не важно, с чем работать – c «чистым» объектом данных или с «обёрнутым».
Применимость
Когда вам нужно добавлять обязанности объектам на лету, незаметно для кода, который их использует.
Объекты помещают в обёртки, имеющие дополнительные поведения. Обёртки и сами объекты имеют одинаковый интерфейс, поэтому клиентам без разницы, с чем работать – с обычным объектом данных или с обёрнутым.
Когда нельзя расширить обязанности объекта с помощью наследования.
Во многих языках программирования есть ключевое слово final , которое может заблокировать наследование класса. Расширить такие классы можно только с помощью Декоратора.
Шаги реализации
- Убедитесь, что в вашей задаче есть один основной компонент и несколько опциональных дополнений или надстроек над ним.
- Создайте интерфейс компонента, который описывал бы общие методы как для основного компонента, так и для его дополнений.
- Создайте класс конкретного компонента и поместите в него основную бизнес-логику.
- Создайте базовый класс декораторов. Он должен иметь поле для хранения ссылки на вложенный объект-компонент. Все методы базового декоратора должны делегировать действие вложенному объекту.
- И конкретный компонент, и базовый декоратор должны следовать одному и тому же интерфейсу компонента.
- Теперь создайте классы конкретных декораторов, наследуя их от базового декоратора. Конкретный декоратор должен выполнять свою добавочную функцию, а затем (или перед этим) вызывать эту же операцию обёрнутого объекта.
- Клиент берёт на себя ответственность за конфигурацию и порядок обёртывания объектов.
Преимущества и недостатки
Преимущества
- Большая гибкость, чем у наследования.
- Позволяет добавлять обязанности на лету.
- Можно добавлять несколько новых обязанностей сразу.
- Позволяет иметь несколько мелких объектов вместо одного объекта на все случаи жизни.
Недостатки
- Трудно конфигурировать многократно обёрнутые объекты.
- Обилие крошечных классов.
Отношения с другими паттернами
Адаптер меняет интерфейс существующего объекта. Декоратор улучшает другой объект без изменения его интерфейса. Причём Декоратор поддерживает рекурсивную вложенность, чего не скажешь об Адаптере.
Адаптер предоставляет классу альтернативный интерфейс. Декоратор предоставляет расширенный интерфейс. Заместитель предоставляет тот же интерфейс.
Цепочка обязанностей и Декоратор имеют очень похожие структуры. Оба паттерна базируются на принципе рекурсивного выполнения операции через серию связанных объектов. Но есть и несколько важных отличий.
Обработчики в Цепочке обязанностей могут выполнять произвольные действия, независимые друг от друга, а также в любой момент прерывать дальнейшую передачу по цепочке. С другой стороны Декораторы расширяют какое-то определённое действие, не ломая интерфейс базовой операции и не прерывая выполнение остальных декораторов.
Компоновщик и Декоратор имеют похожие структуры классов из-за того, что оба построены на рекурсивной вложенности. Она позволяет связать в одну структуру бесконечное количество объектов.
Декоратор оборачивает только один объект, а узел Компоновщика может иметь много детей. Декоратор добавляет вложенному объекту новую функциональность, а Компоновщик не добавляет ничего нового, но «суммирует» результаты всех своих детей.
Но они могут и сотрудничать: Компоновщик может использовать Декоратор, чтобы переопределить функции отдельных частей дерева компонентов.
Архитектура, построенная на Компоновщиках и Декораторах, часто может быть улучшена за счёт внедрения Прототипа. Он позволяет клонировать сложные структуры объектов, а не собирать их заново.
Стратегия меняет поведение объекта «изнутри», а Декоратор изменяет его «снаружи».
Декоратор и Заместитель имеют схожие структуры, но разные назначения. Они похожи тем, что оба построены на принципе композиции и делегируют работу другим объектам. Паттерны отличаются тем, что Заместитель сам управляет жизнью сервисного объекта, а обёртывание Декораторов контролируется клиентом.