Декоратор-класс работающий и для отдельных функций и для методов объектов Python

Основы использования декораторов Python описаны в интернете в тысячах постов на сотнях языков, поэтому я не буду тут повторно описывать основы.

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

Скажем, мы хотим добавить декторатор к методу func_to_wrap:

class ClassToWrap:
    def func_to_wrap(self):
        self.name += '!'
        print(self.name)
        return self.name

Пишем по учебнику класс-декторатор:

class Decorator:
    def __init__(self, orig_func):
        self.orig_func = orig_func

    def __call__(self, *args):
        return self.orig_func(*args)

Пытаемся применить его к методу func_to_wrap и получаем ошибку

class ClassToWrap:
    @Decorator
    def func_to_wrap(self):
        self.name += '!'
        print(self.name)
        return self.name
        
c = ClassToWrap()
c.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.