关于装饰器函数的一些事情

A few things about decorator functions

我正在尝试理解使用装饰器存储已计算数字值的斐波那契数列示例。例如fib(5)会被计算,当我们到达fib(6)时,它不会再计算fib(5)... 我对装饰器有一点了解,但有些事情让我很困惑。我对下面的代码有几个问题。

from functools import wraps
def dec(func):
    values = {}
    @wraps(func)
    def wrap(*args):
        if args not in values:
            values[args] = func(*args)
        return values[args]
    return wrap

@dec
def fib(n):
    if n <= 2:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)
  1. 为什么 *args 用在 wrap() 中?难道不应该只取一个数字 n 并检查它的值是否在字典中吗?为什么args有些地方叫with,有些地方不叫*
  2. 当函数fib被递归调用时会发生什么(装饰函数如何表现)。我首先认为它在每次递归期间都会进入该函数,但这不对,因为值字典会重置?那么它是否只输入 wrap() 函数?
  3. 为什么它 return 在末尾换行?

什么是*args

*args 匹配所有剩余参数作为元组。举个例子:

def f(*args):
    print(args)
f('a', 'b')
# output: ('a', 'b')

在这种情况下,它用于使用完全相同的参数调用内部函数,无论它们可能是什么。您还可以使用双星来匹配关键字参数,目前只有位置参数有效。

递归调用 fib 会发生什么

当用 @ 装饰函数时,引用会立即被覆盖。当 fib 在自身内部调用 fib() 时,它首先在本地范围内查找具有该名称的变量。由于存在 none,它将在下一个范围内查找,在本例中为全局范围。在那里,它找到一个名为 fib 的变量,它实际上是从装饰器分配给 wrap 函数的,原始 fib 的“上下文”是 func

查找闭包以了解有关其工作原理的更多信息。

为什么装饰器return wrap最后

装饰器基本上是用一个函数替换另一个函数。它像函数一样调用 @ 之后的变量,然后用该调用的结果替换由 def 定义的新函数。在这种情况下,您想将其替换为 wrap,一个可能调用也可能不调用旧函数的新函数。

如果你不return任何东西,变量fib将简单地设置为None(默认return值),你不能呼叫 None.

1- 你完全正确。不需要“*”,因为您只检查传递给函数的值。所以简单地称它为“n”。

2-首先让我们弄清楚标签“fib”在你使用“@dec”之后是什么?实际上它现在是装饰器中的内部函数(我的意思是“包装”函数)。为什么 ?因为@dec 实际上是这样做的:

fib = dec(fib)

所以调用了“dec”装饰器,它是什么return? “包装”功能。什么是“换行”功能?这是一个包含“值”字典的闭包。

无论何时调用装饰器,装饰器的主体都只执行一次。所以只有一个“值”字典。在执行“dec”装饰器的主体期间还会发生什么?除了 return 引用“wrap”函数外,别无其他。就是这样。

现在,当您调用“fib”函数(最初是“wrap”函数)时,此闭包正常运行,因为它只是一个递归函数,只是它具有额外的缓存功能。

3- 因为你需要有一个内部函数的句柄(这里是“wrap”函数)。您想稍后调用它以计算 Fibonacci。

只需添加一些打印语句,您就可以很好地了解这里发生的事情,例如:

from functools import wraps
def dec(func):
    values = {}
    @wraps(func)
    def wrap(*args):
        print("args: ", args, " *args:", *args, args not in values, values)
        if args not in values:
            values[args] = func(*args)
        return values[args]
    print("Wrap", wrap)
    return wrap

@dec
def fib(n):
    if n <= 2:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)

print("Answer", fib(5))

所以,这个输出是:

Wrap <function fib at 0x7facac4b70d0>
args:  (5,)  *args: 5 True {}
args:  (4,)  *args: 4 True {}
args:  (3,)  *args: 3 True {}
args:  (2,)  *args: 2 True {}
args:  (1,)  *args: 1 True {(2,): 1}
args:  (2,)  *args: 2 False {(2,): 1, (1,): 1, (3,): 2}
args:  (3,)  *args: 3 False {(2,): 1, (1,): 1, (3,): 2, (4,): 3}
Answer 5

首先从问题的最后一部分开始,正如您从输出中看到的那样,当 Python 在到达print 语句。这只发生一次,它使您随后对 fib 的调用能够调用已经包装的函数。

为了让包装函数工作,它需要知道传递给包装函数的参数是什么,它通过 *args 查看传递的参数,它需要这些参数以便记住它和它的结果。

args*args 之间的区别归结为 tuple unpacking。您可以从上面的输出中看到 args 包含,例如(1, )*args 将其解压缩为 1

因此,通过在包装函数中使用 args,它实际上并没有像您怀疑的那样将数字作为键存储在 values 字典中,而是包含数字的元组。在这种情况下,可以进行解包,但这将是一个不必要的额外步骤。

这也让您很好地了解递归发生的方式,因为在第一次调用 fib(n) 时,return 语句的第一部分是 return fib(n - 1),所以需要在 fib(n-2) 之前进行评估,因此这会立即导致从 n1 的每个值的记忆,然后你return 备份递归堆栈,评估 fib(n-2),但所有这些结果都可以由 values 满足,无需进一步递归调用 fib.