Проблема, с которой вы столкнулись, заключается в том, что декораторы методов класса - это не переданные методы, а функции.
В 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 идет непосредственно в объект класса для их поиска. Это задокументированный выбор реализации, но тем не менее это «странная» асимметрия, которая может стать неожиданностью.