Python 中基于 class 的装饰器中存储的信息

Stored information in class-based decorator in Python

我是 Python 的初学者,正在从 Lutz 的书中学习装饰器。我在下面遇到了这段代码。我不确定 tracer 为什么以及如何保留函数调用的数量,即使创建了新实例也是如此。

class tracer:
    def __init__(self,func):
        self.calls=0
        self.func=func
    def __call__(self, *args):
        self.calls+=1
        print('call %s to %s' %(self.calls,self.func.__name__))
        self.func(*args)

@tracer
def spam(a,b,c):
    print (a+b+c)

spam(1,2,3) #Here calls counter increments to 1
t= tracer(spam)
t.func(3,4,5) # here calls counter increments to 2 even though I called `spam` using new instance `t`

g=tracer(spam)
g.func(4,5,6)  #calls counter increments to 3.

正如我们在上面看到的,即使创建了一个新实例,calls 计数器状态也会保留。

谁能解释一下为什么会这样?我尝试使用 PyCharm 调试代码,似乎 spam 的内存位置保持不变,与特定实例的调用无关。


我正在使用来自 Anaconda Distribution 的 Python 3.6。

实际上,虽然 tg 的赋值确实在创建新实例,但您传递的是原始包装函数 spam 的相同实例。修饰后,spam不再是一个函数,而是tracer的一个实例。这就是 Python 设计来处理对象周围 class 的包装的方式:对象的名称成为包装对象的实例。


任何时候创建 tracer(spam)tracer 中的属性 func 都是原始包装函数 spam 的实例。因此,当调用包装值时,self.func(*args)tracer.__call__ 中被调用,触发 func.__call__,它递增 calls.

tracertg 的实例都被传递给正在分配的 tracerspam 的相同实例到属性 func。因此,t.funcg.func 都是实例,因此引用 spam 及其所有属性。因此,当您调用 t.funcg.func 时,您将触发 spam.__call__,从而在 spam 中递增 calls

class tracer:
   def __init__(self, _func):
     self.func = _func
     self.calls = 0
   def __repr__(self):
     return f"{self.__class__.__name__}(storing {self.func.__name__})"
   def __call__(self, *args):
      self.calls += 1
      print(f"__call__ executed in {repr(self)}")
      return self.func(*args)

@tracer
def spam(a,b,c):
  print (a+b+c)

>>>spam(1, 2, 3)
__call__ executed in tracer(storing spam)
t= tracer(spam)
>>>t.func(1, 2, 3)
__call__ executed in tracer(storing spam)
g=tracer(spam)
>>>g.func(1, 2, 3)
__call__ executed in tracer(storing spam)

您的问题是 tg 是双重包裹的示踪剂。也就是说,它们是一个 tracer 实例包含另一个 tracer 实例(最终引用一个函数)。外部 tracer 并不能正常工作,因为内部 tracer 没有 __name__ 属性,因为它不是函数。您只能调用 t.func,它会绕过外部跟踪器(及其计数)直接调用内部跟踪器。

您可以通过向每个 tracer 添加一个 __name__ 属性来使代码工作:

class tracer:
    def __init__(self,func):
        self.calls=0
        self.func=func
        self.__name__ = 'tracer wrapping %r' % func.__name__   # give ourselves a name
    def __call__(self, *args):
        self.calls+=1
        print('call %s to %s' %(self.calls,self.func.__name__))
        self.func(*args)    # note, you should probably return the result of this call

现在您可以调用 t(3, 4, 5)g(5, 6, 7),每次调用都会打印出两个计数,一个用于内部跟踪器,一个用于外部跟踪器。外部计数将是独立的(每个从 1 开始),但内部计数将被共享(就像您最初看到的那样)。

当然,也可能是您不想要嵌套的示踪剂。在这种情况下,您可能希望从函数之前删除 @tracer 行。这就是应用内​​部跟踪器的地方,它相当于将 spam = tracer(spam) 放在 spam 函数的定义之后。如果没有该行,spam 将直接引用该函数(没有 tracer 包裹它),并且 tg 将直接应用于该函数,而不内心的 tracer 妨碍了。