数据如何在多个调饰函数调用中保持持久性?

时间:2012-08-06 14:49:14

标签: python decorator memoization internals

以下函数用作存储已计算值结果的装饰器。如果之前已经计算过该参数,该函数将返回存储在cache字典中的值:

def cached(f):
    f.cache = {}
    def _cachedf(*args):
        if args not in f.cache:
            f.cache[args] = f(*args)

        return f.cache[args]

    return _cachedf

我意识到(错误地)cache不需要是函数对象的属性。事实上,以下代码也适用:

def cached(f):
    cache = {}   # <---- not an attribute this time!
    def _cachedf(*args):
        if args not in cache:
            cache[args] = f(*args)

        return cache[args]
    return _cachedf

我很难理解cache对象如何在多个调用中保持不变。我尝试多次调用多个缓存函数,但未发现任何冲突或问题。

有人可以帮助我了解即使在返回cache函数后_cachedf变量仍然存在吗?

2 个答案:

答案 0 :(得分:12)

您在此处创建closure:函数_cachedf()将关闭来自封闭范围的变量cache。只要函数对象存在,这就会使cache保持活动状态。

编辑:也许我应该在Python中添加一些关于它如何工作的细节以及CPython如何实现它。

让我们看一个更简单的例子:

def f():
    a = []
    def g():
        a.append(1)
        return len(a)
    return g

交互式解释器中的示例用法

>>> h = f()
>>> h()
1
>>> h()
2
>>> h()
3

在编译包含函数f()的模块期间, 编译器看到函数g()引用了名称a 封闭范围并在代码中记住此外部引用 对应于函数f()的对象(具体来说,它添加了 将a命名为f.__code__.co_cellvars)。

那么调用函数f()会发生什么?第一行 创建一个新的列表对象并将其绑定到名称a。下一行 创建一个新的函数对象(使用在...期间创建的代码对象) 模块的编译)并将其绑定到名称g。身体 此时不执行g(),最后是funciton对象 归还。

由于f()的代码对象有一个名称为a的注释 在本地函数引用时,会创建此名称的“单元格” 输入f()。此单元格包含对实际列表的引用 对象a绑定,函数g()获取引用 这个细胞。这样,列表对象和单元格甚至保持活跃 当函数f()退出时。

答案 1 :(得分:3)

  

有人可以帮我理解即使在返回_cachedf函数后缓存变量仍然存在吗?

它与Python的引用计数垃圾收集器有关。 cache变量将被保存并可访问,因为函数_cachedf具有对它的引用,而cached的调用者具有对它的引用。再次调用该函数时,您仍然使用最初创建的相同函数对象,因此您仍然可以访问缓存。

在销毁所有引用之前,不会丢失缓存。您可以使用del运算符来执行此操作。

例如:

>>> import time
>>> def cached(f):
...     cache = {}   # <---- not an attribute this time!
...     def _cachedf(*args):
...         if args not in cache:
...             cache[args] = f(*args)
...         return cache[args]
...     return _cachedf
...     
... 
>>> def foo(duration):
...     time.sleep(duration)
...     return True
...     
... 
>>> bob = cached(foo)
>>> bob(2) # Takes two seconds
True
>>> bob(2) # returns instantly
True
>>> del bob # Deletes reference to bob (aka _cachedf) which holds ref to cache
>>> bob = cached(foo)
>>> bob(2) # takes two seconds
True
>>> 

为了记录,你想要实现的是Memoization,并且decorator pattern page有一个更完整的memoizing装饰器可以做同样的事情,但是使用装饰器即可。您的代码和基于类的装饰器基本相同,基于类的装饰器在存储之前检查哈希能力。


编辑(2017-02-02):@ {SiminJie评论cached(foo)(2)总是会导致延迟。

这是因为cached(foo)返回带有新缓存的新函数。调用cached(foo)(2)时,会创建一个新的(空)缓存,然后立即调用缓存的函数。

由于缓存为空且无法找到值,因此会重新运行基础函数。相反,请执行cached_foo = cached(foo),然后多次调用cached_foo(2)。这只会导致第一次通话的延迟。此外,如果用作装饰器,它将按预期工作:

@cached
def my_long_function(arg1, arg2):
  return long_operation(arg1,arg2)

my_long_function(1,2) # incurs delay
my_long_function(1,2) # doesn't

如果您不熟悉装饰器,请查看this answer以了解上述代码的含义。