различное поведение при реализации декоратора в Python с функцией или классом - PullRequest
2 голосов
/ 12 июля 2020

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

вот простой пример:

def decorator(method):
    def inner(ref, *args, **kwargs):
        print(f'do something with {method.__name__} from class {ref.__class__}')
        return method(ref, *args, **kwargs)

    return inner


class class_decorator:

    def __init__(self, method):
        self.method = method

    def __call__(self, *args, **kwargs):
        print('before')
        result = self.method(*args, **kwargs)
        print('after')
        return result


class test:

    #@decorator
    @class_decorator
    def pip(self, a):
        return a + 1

    #@decorator
    @class_decorator
    def pop(self, a):
        result = a + self.pip(a)
        return result

t = test()
    
print(f'result pip : {t.pip(3)}')
print(f'result pop : {t.pop(3)}')

Это будет работать с функцией 'декоратора' но не с class_decorator, потому что вызов nest в методе pop

Ответы [ 2 ]

1 голос
/ 12 июля 2020

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

В Python метод и функция имеют два разных типа:

Python 3.8.3 (default, May 17 2020, 18:15:42)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.15.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: class X:
   ...:     def m(self, *args, **kwargs):
   ...:         return [self, args, kwargs]

In [2]: type(X.m)
Out[2]: function

In [3]: type(X().m)
Out[3]: method

In [4]: X.m(1,2,x=3)
Out[4]: [1, (2,), {'x': 3}]

In [5]: X().m(1,2,x=3)
Out[5]: [<__main__.X at 0x7f1424f33a00>, (1, 2), {'x': 3}]

Преобразование "magi c" из функции (поскольку m находится в X) в метод (каким он становится при поиске в экземпляре X()) происходит, когда m ищется в пример. Не найденный в самом экземпляре, Python ищет его в классе, но когда он обнаруживает, что это функция, значение, возвращаемое тому, кто запросил X().m, «завернуто» в объект метода, который включает значение self.

Проблема, с которой вы столкнулись, заключается в том, что это преобразование magi c применяется только в том случае, если искомое значение оказывается объектом function. Если это экземпляр класса, реализующего __call__ (как в вашем случае), обертывания не происходит, и поэтому необходимое значение self не привязано, и окончательный код не работает.

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

In [1]: def deco(f):
   ...:     state = [0]
   ...:     def decorated(*args, **kwargs):
   ...:         state[0] += 1
   ...:         print(state[0], ": decorated called with", args, **kwargs)
   ...:         res = f(*args, **kwargs)
   ...:         print("return value", res)
   ...:         return res
   ...:     return decorated

In [2]: class X:
   ...:     def __init__(self, x):
   ...:         self.x = x
   ...:     @deco
   ...:     def a(self):
   ...:         return self.x + 1
   ...:     @deco
   ...:     def b(self):
   ...:         return 10 + self.a()

In [3]: x = X(12)

In [4]: x.a()
1 : decorated called with (<__main__.X object at 0x7f30a76f41c0>,)
return value 13
Out[4]: 13

In [5]: x.a()
2 : decorated called with (<__main__.X object at 0x7f30a76f41c0>,)
return value 13
Out[5]: 13

In [6]: x.b()
1 : decorated called with (<__main__.X object at 0x7f30a76f41c0>,)
3 : decorated called with (<__main__.X object at 0x7f30a76f41c0>,)
return value 13
return value 23
Out[6]: 23

В приведенном выше примере я использовал простой список state, но вы можете использовать любое количество состояний, включая экземпляры классов. Однако важным моментом является то, что декораторы возвращают объект function. Таким образом, при поиске в экземпляре класса среда выполнения Python создаст правильный объект method, чтобы вызовы методов работали.

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

Еще один факт, который может быть не очевиден и укусил меня в прошлом, - это специальные методы вроде __call__ или __add__ НЕ просматриваются сначала в экземпляре, а Python идет непосредственно в объект класса для их поиска. Это задокументированный выбор реализации, но тем не менее это «странная» асимметрия, которая может стать неожиданностью.

0 голосов
/ 12 июля 2020

Декоратор - это просто «синтаксис c сахар». Проблема с декоратором класса заключается в том, что self больше не передается в качестве первого аргумента.

Мы хотим, чтобы c имитировал поведение decorator, в котором мы возвращаем methd , которому больше не нужно передавать self.

Это то, что можно сделать напрямую с помощью функции partial, превратив ее в дескриптор

Вы заметите, что первая вызываемая функция - это __get__

class class_decorator:

    def __init__(self, method):
        self.method = method
        
    def __set_name__(self, owner, name):
        self.owner = owner

    def __call__(self, *args, **kwargs):
        print('before')
        result = self.method(*args,**kwargs)
        print('after')
        return result
        
    def __get__(self, instance, owner):
        print('calling get')
        from functools import partial
        return partial(self, instance)
...