将 memoization 装饰器理解为闭包

Posted

技术标签:

【中文标题】将 memoization 装饰器理解为闭包【英文标题】:Understanding a memoization decorator as a closure 【发布时间】:2017-01-04 03:37:18 【问题描述】:

我正在尝试学习 Python 装饰器,我想更详细地了解闭包是如何应用的,例如在这个 memoization 上下文中:

def memoize(f):
    memo = 
    def helper(x):                 
        if x not in memo:
            memo[x] = f(x)
        return memo[x]
    return helper   

@memoize  
def fib(n):
    if n in (0,1):
        return n
    return fib(n - 1) + fib(n - 2)

我了解memoize 返回的函数绑定到helper 封闭范围内的memo 值,即使程序流不再在该封闭范围内。因此,如果memoize 被重复调用,它将根据memo 的当前值返回不同的函数序列。我也知道@memoize 是一种语法糖,它会导致对fib(n) 的调用被对memoize(fib(n)) 的调用替换。

我苦苦挣扎的地方是fib(n)n 的调用值如何有效地转换为helper(x) 中的x。大多数关于闭包的教程似乎都没有明确说明这一点,或者他们只是含糊地说一个函数“关闭”另一个函数,这听起来就像魔术一样。我可以了解如何使用语法,但希望更好地掌握这里发生的确切情况以及代码执行时传递的对象和数据。

【问题讨论】:

“fib(n)中n的被调用值如何有效转换为helper(x)中的x”?我想你已经把它弄反了。例如,当有人调用fib(5) 时,实际调用的是helper(5),所以x5helper 然后调用(真正的)fib,即memo[5] = f(5),所以现在n 也是5。这些是正常的函数调用。绝对没有魔法。 我在 (***.com/questions/739654/…) 看到了关于装饰器的非常好的答案,但这似乎并没有解决我的特殊困难 @MichaelMaggs:您误解了装饰器的工作原理,您可能需要重新阅读该帖子。 【参考方案1】:

您误解了 @memoize 装饰器的作用。每次调用fib 时都不会调用装饰器。每个被装饰的函数只调用一次装饰器。

原来的fib 函数被helper() 函数替换,原来的函数对象可作为f 闭包,连同memo 闭包:

>>> def fib(n):
...     if n in (0,1):
...         return n
...     return fib(n - 1) + fib(n - 2)
...
>>> fib
<function fib at 0x1023398c0>
>>> memoize(fib)
<function helper at 0x102339758>

这提供了替换 helper 函数 one 闭包,并且每次调用该 helper() 函数时,都会使用相同的 memo 字典来查找当前值的已生成结果x.

你可以在解释器中看到这一切:

>>> @memoize
... def fib(n):
...     if n in (0,1):
...         return n
...     return fib(n - 1) + fib(n - 2)
...
>>> fib
<function helper at 0x10232ae60>

注意那里的函数名!那是helper。它有一个闭包:

>>> fib.__closure__
(<cell at 0x10232d9f0: function object at 0x10232ade8>, <cell at 0x10232da98: dict object at 0x102353050>)

一个函数对象和一个字典,它们分别是fmemo。使用.cell_contents访问单元格内容:

>>> fib.__closure__[0].cell_contents
<function fib at 0x10232ade8>
>>> fib.__closure__[1].cell_contents

那是原始的装饰函数和空字典。让我们使用这个函数:

>>> fib(0)
0
>>> fib(1)
1
>>> fib.__closure__[1].cell_contents
0: 0, 1: 1

这就是将 x 设置为 01 的结果值存储在 memo 字典中。下次我用任何一个值调用fib 时,都会使用备忘录。计算并添加新的x 值:

>>> fib(2)
1
>>> fib.__closure__[1].cell_contents
0: 0, 1: 1, 2: 1

你可以通过计算一个较大的数字需要多长时间来了解它的效果:

>>> start = time.clock(); fib(500); print(format(time.clock() - start, '.10f'))
139423224561697880139724382870407283950070256587697307264108962948325571622863290691557658876222521294125L
0.0008390000
>>> start = time.clock(); fib(500); print(format(time.clock() - start, '.10f'))
139423224561697880139724382870407283950070256587697307264108962948325571622863290691557658876222521294125L
0.0000220000

>>> start = time.clock(); fib(500); print(format(time.clock() - start, '.10f'))
139423224561697880139724382870407283950070256587697307264108962948325571622863290691557658876222521294125L
0.0000190000

第一次调用后,查找备忘录的速度是所有中间结果都被记忆的 40 倍:

>>> len(fib.__closure__[1].cell_contents)
501

【讨论】:

啊哈!这样就很清楚了,谢谢。我确实认为装饰器被反复调用。

以上是关于将 memoization 装饰器理解为闭包的主要内容,如果未能解决你的问题,请参考以下文章

轻松理解python中的闭包和装饰器 (下)

python中“生成器”“迭代器”“闭包”“装饰器”的深入理解

理解python中的装饰器

python-装饰器

python闭包和装饰器的理解

python 使用Python装饰器进行Memoize