关于装饰器函数的一些事情
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)
- 为什么
*args
用在 wrap()
中?难道不应该只取一个数字 n 并检查它的值是否在字典中吗?为什么args
有些地方叫with,有些地方不叫*
?
- 当函数
fib
被递归调用时会发生什么(装饰函数如何表现)。我首先认为它在每次递归期间都会进入该函数,但这不对,因为值字典会重置?那么它是否只输入 wrap()
函数?
- 为什么它 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)
之前进行评估,因此这会立即导致从 n 到 1 的每个值的记忆,然后你return 备份递归堆栈,评估 fib(n-2)
,但所有这些结果都可以由 values
满足,无需进一步递归调用 fib
.
我正在尝试理解使用装饰器存储已计算数字值的斐波那契数列示例。例如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)
- 为什么
*args
用在wrap()
中?难道不应该只取一个数字 n 并检查它的值是否在字典中吗?为什么args
有些地方叫with,有些地方不叫*
? - 当函数
fib
被递归调用时会发生什么(装饰函数如何表现)。我首先认为它在每次递归期间都会进入该函数,但这不对,因为值字典会重置?那么它是否只输入wrap()
函数? - 为什么它 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)
之前进行评估,因此这会立即导致从 n 到 1 的每个值的记忆,然后你return 备份递归堆栈,评估 fib(n-2)
,但所有这些结果都可以由 values
满足,无需进一步递归调用 fib
.