试图理解 Python 包装器

Posted

技术标签:

【中文标题】试图理解 Python 包装器【英文标题】:Trying to understand a Python wrapper 【发布时间】:2020-08-30 14:13:40 【问题描述】:

对于下面的功能,我正在努力理解

我。为什么wrapper.count = 0 在包装函数下面初始化?为什么不在 def counter(func) 下面初始化?为什么 wrapper.count 不将 wrapper.count 重置为 0,因为它在 wrapper 函数下方运行?

我想了解wrapper.count 是什么?为什么不初始化一个普通变量count 而不是wrapper.count

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

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

【问题讨论】:

" 为什么不在 def counter(func) 下面初始化" 因为wrapper 还没有定义。 【参考方案1】:

装饰器有错误,你需要在包装函数内部:

return func(*args, **kwargs)  # instead of `return func`

为什么wrapper.count = 0在包装函数下面初始化?

因为如果您在包装函数内部执行此操作,那么它将始终将 wrapper.count 的值重置为 0。除非您检查它是否尚未定义。 (我在回答的最后给出了一个例子。)

为什么不在def counter(func)下面初始化?

因为这里没有定义包装函数。所以口译员会抱怨的。

为什么wrapper.count 不将wrapper.count 重置为0,因为它是在包装函数下面执行的?

因为当你用@counter装饰器包装一个函数时,这条语句只执行一次,而且它不会在你每次调用foo()函数时执行。

我想了解wrapper.count 是什么?

这是一个函数属性。或多或少类似于 C++ 等函数中的 static 变量。

为什么不直接初始化一个普通变量count 而不是wrapper.count

因为这将是一个局部变量,它会在每次调用时将 count 重置为 0。


还有另一种方法可以在包装函数中定义wrapper.count = 0。所以现在你不需要在wrapper 函数之外定义它。

def counter(func):
  def wrapper(*args, **kwargs):
    if not hasattr(wrapper, 'count'):
        wrapper.count = 0
    wrapper.count += 1
    return func(*args, **kwargs)
  return wrapper

【讨论】:

@MadPhysicist 已修复 另外,hasattrin __dict__ 更健壮 很好地解决了所有问题。最后一个挑剔。在 C++ 中与静态的类比有点不确定。 Python 函数是第一类对象,是 FunctionType 的实例,因此可以像任何其他对象一样具有属性。 @MadPhysicist 是的,你是对的。我已经在上下文中解释了这个概念到这里的使用。它就像一个计数器,每次调用后都不会重置,而是保存之前的值。就像我们在 C++ 中通过在函数中定义 static 变量一样。 @warvariuc。你真的在乎他的消息来源是什么吗?还是你在乎他比你得更多分?【参考方案2】:

在高层次上,装饰函数维护一个计数器,记录它被调用的次数。

代码存在一个主要问题。包装器实际上并没有按应有的方式调用包装的函数。而不是return func,它只返回函数对象,它应该是

return func(*args, **kwargs)

作为@warvariuc points out,一个可能的原因是作者没有或不知道nonlocal,它可以让您访问封闭的命名空间。

我认为更合理的原因是您希望能够访问柜台。函数是具有可变字典的一流对象。您可以分配和访问它们的任意属性。打几个电话后检查foo.count可能会很方便,否则为什么要维护它呢?

wrapper.counter 以这种方式初始化的原因仅仅是 wrapper 不存在于本地命名空间中,直到运行 def 语句来创建它。每次运行counter 时,内部def 都会创建一个新的函数对象。 def 一般是每次运行都会创建一个函数对象的赋值。

关于您显示的代码的另一个小问题是 foo.__name__ 在装饰后将是 wrapper 而不是 foo。为了缓解这种情况,并使其更忠实地模仿原始功能,您可以使用functools.wraps,它是装饰器包装器的装饰器。您的代码如下所示:

from functools import wraps

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

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

现在你可以做

>>> foo.__name__
'foo'
>>> foo()
calling foo()
>>> foo()
calling foo()
>>> foo()
calling foo()
>>> foo.count
3

【讨论】:

【参考方案3】:

为什么不初始化一个普通的变量计数而不是variable.count

我的猜测是这种模式首先出现在 Python 2 中,其中 nonlocal 语句不可用。在我看来,sn-p 的作者只是试图模拟 C 语言中的静态变量(https://***.com/a/279586/248296)。

因为如果您尝试使用在函数 counter 的顶层声明的普通变量,您将无法在 wrapper 内对其进行赋值。

如果您将count 放在counter 下方,您会将其设为全局,因此它将在装饰器的所有实例之间共享,这可能不是所需的行为:

count = 0

def counter(func):

  def wrapper(*args, **kwargs):
    global count
    count += 1
    return func(*args, **kwargs)

  return wrapper

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

这是一个带有nonlocal(Python 3+)的版本:

def counter(func):

  def wrapper(*args, **kwargs):
    nonlocal count
    count += 1
    # Call the function being decorated and return the result
    return func(*args, **kwargs)

  count = 0
  # Return the new decorated function
  return wrapper

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

【讨论】:

如果我正确理解目的,非本地将无济于事 我认为使用 nonlocal 会产生相同的效果。请参阅答案的更新。 @MadPhysicist 我已经发布了一个答案,说明为什么您可能希望以原始方式进行操作,而不管非本地情况如何。您无法轻松访问它创建的闭包,使您的计数器纯粹供内部使用。

以上是关于试图理解 Python 包装器的主要内容,如果未能解决你的问题,请参考以下文章

通过 python 扩展/包装器传递浮点数组指针 – SndObj-library

为啥 Python 的装饰器语法比普通的包装器语法提供更快的记忆代码?

Python装饰器

为啥某些python代码的包装器和包装函数是相同的。

python nonlocal的理解使用

Python 高效包装器