了解这个 Python 装饰器是如何工作的

Posted

技术标签:

【中文标题】了解这个 Python 装饰器是如何工作的【英文标题】:Understanding how this Python Decorator works 【发布时间】: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))

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

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

【问题讨论】:

响应第3点,包装函数仅在第一次修饰foo时调用,之后包装函数是包装foo的唯一函数,因此不会执行该行跨度> 【参考方案1】:

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

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

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

【讨论】:

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

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

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

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

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

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

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

    每次调用 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.

【讨论】:

以上是关于了解这个 Python 装饰器是如何工作的的主要内容,如果未能解决你的问题,请参考以下文章

理解Python装饰器

es6 中的装饰器是如何工作的?

python之装饰器

python装饰器是啥意思

Python - 所有内置装饰器是啥? [关闭]

python 装饰器从放弃到玩转(初级)