【问题标题】:Python decorators count function callPython 装饰器计数函数调用
【发布时间】:2017-12-11 14:07:08
【问题描述】:

我正在刷新我对一些我还没有得到的 python 特性的记忆,我正在向this python tutorial 学习,并且有一个我不完全理解的例子。这是关于一个装饰器计算对函数的调用,这里是代码:

def call_counter(func):
    def helper(x):
        helper.calls += 1
        return func(x)
    helper.calls = 0
    return helper

@call_counter
def succ(x):
    return x + 1

if __name__ == '__main__':
    print(succ.calls)
    for i in range(10):
        print(succ(i))
    print(succ.calls)

我在这里不明白的是,为什么我们要增加函数包装器的调用 (helper.calls += 1) 而不是函数调用本身,为什么它实际上有效?

【问题讨论】:

  • @call_counter succ == succ = call_counter(succ) = helper

标签: python python-decorators


【解决方案1】:

关于装饰器要记住的重要一点是,装饰器是一个函数,它接受一个函数作为参数,并返回另一个函数。返回的值 - 又一个函数 - 将在调用原始函数的名称时调用。

这个模型可以很简单:

def my_decorator(fn):
    print("Decorator was called")
    return fn

在这种情况下,返回的函数与传入的函数相同。但这通常不是你所做的。通常,您要么返回一个完全不同的函数,要么返回一个以某种方式链接或包装原始函数的函数。

在您的示例中,这是一个非常常见的模型,您有一个返回的内部函数:

def helper(x):
    helper.calls += 1
    return func(x)

这个内部函数调用原始函数 (return func(x)),但它也会增加调用计数器。

这个内部函数被插入作为任何正在装饰的函数的“替换”。因此,当查找您的模块foo.succ() 函数时,结果是对装饰器返回的内部辅助函数的引用。该函数增加调用计数器,然后调用最初定义的succ 函数。

【讨论】:

    【解决方案2】:

    当你装饰一个你“替换”的函数时,你就是使用包装器的函数。

    在这个例子中,在修饰之后,当你调用succ时,你实际上是在调用helper。因此,如果您计算调用次数,则必须增加 helper 调用次数。

    您可以通过检查装饰函数的属性 _name_ 来检查装饰函数后,名称是否已绑定到包装器:

    def call_counter(func):
        def helper(*args, **kwargs):
            helper.calls += 1
            print(helper.calls)
            return func(*args, **kwargs)
        helper.calls = 0
        return helper
    
    @call_counter
    def succ(x):
        return x + 1
    
    succ(0)
    >>> 1
    succ(1)
    >>> 2
    print(succ.__name__)
    >>> 'helper'
    print(succ.calls)
    >>> 2
    

    【讨论】:

    • 我相信 **args 实际上应该是 **kwargs 就像在关键字参数中一样。
    • 我正在做一个记录器装饰器。有没有办法知道它里面有多少层,比如说,装饰的 func A 调用装饰的 func B,有一个变量告诉我 func A 是 1 级而 func B 是 2 级?
    【解决方案3】:

    我在这里不明白的是,为什么我们要增加函数包装器的调用 (helper.calls += 1) 而不是函数调用本身,为什么它实际上可以工作?

    我认为让它成为一个通用的装饰器。你可以这样做

    def succ(x):
        succ.calls += 1
        return x + 1
    
    if __name__ == '__main__':
        succ.calls = 0
        print(succ.calls)
        for i in range(10):
            print(succ(i))
        print(succ.calls)
    

    它工作得很好,但是您需要将.calls +=1 放入您想要应用它的每个函数中,并在运行它们之前初始化为 0。如果你有一大堆想要计算的函数,这肯定会更好。另外,它在定义时将它们初始化为 0,这很好。

    据我了解,它之所以有效,是因为它将函数 succ 替换为装饰器中的 helper 函数(每次装饰函数时都会重新定义)所以 succ = helpersucc.calls = helper.calls。 (当然,名称助手只在装饰器的命名空间中定义)

    这有意义吗?

    【讨论】:

      【解决方案4】:

      据我了解(如果我错了,请纠正我)您的程序执行顺序是:

      1. 注册call_function
      2. 注册succ
      3. 在注册 succ 函数解释器时发现一个装饰器,因此它执行 call_function
      4. 您的函数返回一个作为函数的对象 (helper)。并添加到此对象字段calls
      5. 现在您的函数succ 已分配给helper。所以当你调用你的函数时,你实际上是在调用 helper 函数,封装在一个装饰器中。因此,您添加到辅助函数的每个字段都可以通过寻址succ 在外部访问,因为这两个变量指的是同一件事。
      6. 所以当你打电话给succ()时,如果你打电话helper(*args, **argv)基本上是一样的

      看看这个:

      def helper(x):
          helper.calls += 1
          return 2
      helper.calls = 0
      
      def call_counter(func):
          return helper
      
      @call_counter
      def succ(x):
          return x + 1
      
      if __name__ == '__main__':
          print(succ == helper)  # prints true.
      

      【讨论】:

        【解决方案5】:

        类装饰器示例

        当您使用 类装饰器 装饰函数时,每个函数都有自己的 call_count。这就是 OOP 的简单性。每次调用CallCountDecorator对象,都会增加自己的call_count属性并打印出来。

        class CallCountDecorator:
            """
            A decorator that will count and print how many times the decorated function was called
            """
        
            def __init__(self, inline_func):
                self.call_count = 0
                self.inline_func = inline_func
        
            def __call__(self, *args, **kwargs):
                self.call_count += 1
                self._print_call_count()
                return self.inline_func(*args, **kwargs)
        
            def _print_call_count(self):
                print(f"The {self.inline_func.__name__} called {self.call_count} times")
        
        
        @CallCountDecorator
        def function():
            pass
        
        
        @CallCountDecorator
        def function2(a, b):
            pass
        
        
        if __name__ == "__main__":
            function()
            function2(1, b=2)
            function()
            function2(a=2, b=3)
            function2(0, 1)
        
        # OUTPUT
        # --------------
        # The function called 1 times
        # The function2 called 1 times
        # The function called 2 times
        # The function2 called 2 times
        # The function2 called 3 times
        

        【讨论】:

        • 正如目前所写,您的答案尚不清楚。请edit 添加其他详细信息,以帮助其他人了解这如何解决所提出的问题。你可以找到更多关于如何写好答案的信息in the help center
        猜你喜欢
        • 2017-06-05
        • 2017-01-15
        • 1970-01-01
        • 2017-08-01
        • 2018-03-07
        • 2019-11-07
        • 2010-09-21
        • 2020-03-19
        • 1970-01-01
        相关资源
        最近更新 更多