Декоратор-класс работающий и для отдельных функций и для методов объектов Python
Основы использования декораторов Python описаны в интернете в тысячах постов на сотнях языков, поэтому я не буду тут повторно описывать основы.
После того, как вы поняли основы, вас может ввести в тупик казалось бы простая задача. Мы хотим написать декоратор в виде класса, и применить этот декоратор к методу класса.
Скажем, мы хотим добавить декторатор к методу func_to_wrap
:
Пишем по учебнику класс-декторатор:
Пытаемся применить его к методу func_to_wrap
и получаем ошибку
func_to_wrap() missing 1 required positional argument: 'self'
Секрет в том что Decorator.__call__
вызывает func_to_wrap
не передавая ей self
- тот self
что
получил Decorator.__call__
это его собственный экземляр (объект класса Decorator
), а экземляр
ClassToWrap
вообще нигде не передается. Основная причина в том что orig_func
это unbound
,
не привязанный к экземпляру объекта, а требующий указания экземпляра в первом аргументе.
На этом можно было бы и закончить - так устроены дектораторы-классы. Если вам нужен декоратор метода
объекта, то используйте декораторы-функции, там все будет работать ожидаемо - первый аргумент будет self
декорируемого объекта ClassToWrap
.
Но решить задачу можно и с помощью декоратора-класса. Более того, можно даже сделать универсальное решение, которое можно использовать и как декторатор отдельных функций, и как декоратор методов объектов.
Для этого мы можем задействовать механизм дескрипторов. Напомню, что если атрибут объекта имеет метод __get__
то при обращении к нему Python вернет не сам этот объект, а то что возвращает метод __get__
.
Ниже приведен работающий код.
В нем метод UniversalDecorator.__call__
будет вызываться при декорировании функций.
Если же вы декорируете метод объекта, то Python не будет его вызывать - он вызовет __get__
и в ответ получит
экземпляр WrapperHelper
в который мы уже закинули ссылки как на экземляр ClassToWrap
так и на Decorator
.
Далее Python вызовет результат __get__
как функцию, а поскольку это объект с методом
__call__
(class WrapperHelper
) он вызовет метод
WrapperHelper.__call__
.
Он вызывает объект-декоратор, что означает вызов его __call__
. В этот вызов WrapperHelper.__call__
подставит экземпляр декорируемого класса ClassToWrap
, который попадет как первый элемент в *args
(строка
6). И у нас произойдет корректный вызов декорированного метода func_to_wrap
- первым ее аргументом будет
экземпляр класса ClassToWrap
.