【问题标题】:Understanding how this Python Decorator works了解这个 Python 装饰器是如何工作的
【发布时间】:2020-10-15 23:30:19
【问题描述】:

我一直在研究如何创建自己的装饰器,并给出了以下示例:

def counter(func):
  def wrapper(*args, **kwargs):
    wrapper.count += 1
    # Call the function being decorated and return the result
    return wrapper.count
  wrapper.count = 0
  # Return the new decorated function
  return wrapper

# Decorate foo() with the counter() decorator
@counter
def foo():
  print('calling foo()')
  
foo()
foo()

print('foo() was called {} times.'.format(foo.count))

我不明白那段代码的逻辑。

  1. 如何在自身内部引用函数 (wrapper.count)?
  2. 包装器在定义包装器之前如何计算方法计数?
  3. 每次调用 foo() 时不应该执行 line wrapper.count = 0 吗?

【问题讨论】:

  • 响应第3点,包装函数仅在第一次修饰foo时调用,之后包装函数是包装foo的唯一函数,因此不会执行该行跨度>

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


【解决方案1】:

wrapper.count() 只是wrapper 命名空间中的一个变量。它在包装函数之外用wrapper.count = 0 定义,并在函数被修饰时执行。所以它不是 wrapper() 的方法,而是一个 globalwrapper() 的变量。每次调用foo(),都会执行wrapper()函数,即增加计数器。

您可以将注释# Call the function ... 替换为func() 对函数的实际调用,这样它就会显示foo() 的打印输出。

装饰器不是那么容易理解的。这是一个链接,可能会帮助您了解到底发生了什么:https://realpython.com/primer-on-python-decorators/

【讨论】:

    【解决方案2】:
    1. 如何在自身内部引用函数 (wrapper.count)?

    函数体只有在你调用它时才会被执行。到那时,该功能已经定义好了,所以这就是使之成为可能的原因。以下不会给你任何错误,除非你调用它:

    >>> def foo():
    ...     non_existing_function()
    ...
    

    而且每当你进入foo的正文时,foo已经定义好了,所以你可以引用它。这也是递归调用成为可能的原因。

    1. 包装器在定义包装器之前如何计算方法计数?

    问题也可能是“如何在 wrapper.count 初始化之前增加它?

    但同样,我们可以用同样的方式回答这个问题:因为函数体在我们调用它们之前不会执行,所以wrapper.countwrapper.count += 1 之前被初始化为 0。

    1. 每次调用 foo() 时不应该执行 line wrapper.count = 0 吗?

    让我们看看发生了什么。你写过:

    @counter
    def foo():
      print('calling foo()')
    

    这只是一个语法糖:

    foo = counter(foo)
    

    现在,我们以foo 作为参数调用counter 函数。 counter 是做什么的?

    def counter(func):
      def wrapper(*args, **kwargs):
        wrapper.count += 1
        # Call the function being decorated and return the result
        return wrapper.count
      wrapper.count = 0
      # Return the new decorated function
      return wrapper
    

    在人类语言中,

    • 定义一个名为 wrapper 的函数,它接受未知数量的位置和关键字参数
    • 0 分配为count 的属性,用于wrapper 函数
    • 返回wrapper给调用者

    当我们将结果分配回foo 函数时,我们实际上已将wrapper 分配给foo。因此,当我们调用foo 时,我们实际上是在调用wrapperwrapper.count = 0 行在 wrapper 函数之外,所以它不会在我们每次调用 foo 时运行。

    最后,我强烈建议您观看有关装饰器的精彩 PyCon talk by Reuven M. Lerner

    编辑:我没有阅读包装器的主体,这实际上证明您并不需要知道包装器内部是什么。我的解释仍然是正确的。但是,正如@Mark Tolonen's answer 中所建议的那样,您的包装器可能应该返回func(*args,**kwargs) wrapper.count

    【讨论】:

      【解决方案3】:

      包装不正确。 return wrapper.count 是错误的。正如评论所述,它应该返回带有参数的函数调用的结果;否则, foo 将返回每次调用的次数,而不是其实际结果。

      def counter(func):
          def wrapper(*args, **kwargs):    # "wrapper" function now exists
              wrapper.count += 1           # count doesn't exist yet, but will when wrapper is called.
              return func(*args,**kwargs)  # call the wrapped function and return result
          wrapper.count = 0                # init the attribute on the function
          return wrapper
      
      # Every time "counter" is used, it defines a *different* wrapper function
      # with its own localized "count" variable initialized to zero.
      @counter
      def foo(a,b):
        print(f'foo({a,b})')   # demonstrate that foo was called with the correct arguments.
        return a + b
      
      @counter
      def bar(a,b):
        print(f'bar({a,b})')   # demonstrate that bar was called with the correct arguments.
        return a * b
      
      print(foo(1,2))
      print(foo(b=3,a=4))     # keywords work, too.
      print(bar(5,b=6))
      
      print(f'foo() was called {foo.count} times.')
      print(f'bar() was called {bar.count} times.')
      

      输出:

      foo((1, 2))
      3
      foo((4, 3))
      7
      bar((5, 6))
      30
      foo() was called 2 times.
      bar() was called 1 times.
      

      【讨论】:

        猜你喜欢
        • 2011-07-25
        • 2016-01-06
        • 1970-01-01
        • 2017-10-21
        • 2016-01-24
        • 2020-04-08
        • 2017-05-04
        • 1970-01-01
        • 2011-07-05
        相关资源
        最近更新 更多