【问题标题】:Stored information in class-based decorator in PythonPython中基于类的装饰器中存储的信息
【发布时间】:2018-07-15 19:43:15
【问题描述】:

我是 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。

【问题讨论】:

    标签: python python-3.x decorator python-decorators


    【解决方案1】:

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


    无论何时创建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)
    

    【讨论】:

    • 感谢您的帮助。如果您能解释为什么self.func(*args)tracer.__call__ 中被调用,那就太好了,尽管我创建了tracer 的新实例。如果你能用一个例子来解释这一点,那就太好了。我对 Python 世界很陌生,并且一直在努力自学。
    • @watchtower 请看我最近的编辑。我在tracer 中创建了一个repr 方法来演示在__call__ 中调用了什么对象。如您所见,每次调用t.funcg.func 时,都会访问实例spam
    【解决方案2】:

    您的问题是 tg 是双重包装的示踪剂。也就是说,它们是一个 tracer 实例,其中包含另一个 tracer 实例(它最终指的是一个函数)。外部tracers 并不能真正正常工作,因为内部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 进入方式。

    【讨论】:

    • 感谢 Blckknight。你能解释一下为什么t and g are doubly wrapped tracers?它们不是独立的对象吗?
    • 这是因为spam 已经被@tracer 包裹了一次。当您执行 t = tracer(spam) 时,您将在第一个包装器周围放置第二个包装器。
    • @Blckknight。谢谢你的帮助。有什么方法可以跟踪(或打印)这两个包装器?我尝试调试代码,但我无法理解这些双重包装的装饰器。提前致谢。
    • 当然,spam 是内部包装器。 tg 是两个不同的外包装。您还可以使用t.funcg.func 访问内部包装。
    猜你喜欢
    • 2023-03-17
    • 2018-11-17
    • 2017-03-08
    • 1970-01-01
    • 2014-01-01
    • 2019-09-20
    • 2010-10-14
    • 2022-12-01
    相关资源
    最近更新 更多