2012-08-06 53 views
6

以下函数用作存储已计算值结果的装饰器。 (错误),其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函数返回后仍然存在吗?

回答

11

您在此处创建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来自 封闭示波器并将此外部参考存储在代码 object cor响应功能f()(具体而言,它将 名称a添加到f.__code__.co_cellvars)。

那么调用函数f()时会发生什么?第一行 创建一个新的列表对象并将其绑定到名称a。下一行 创建一个新的函数对象(使用在编译该模块期间创建的代码对象),并将其绑定到名称gg()的主体 此时不执行,最后返回功能对象 。

由于f()代码对象有一张纸条,上面的名字是a通过 本地函数引用,当进入 f()创建一个“细胞”这个名字。该单元格包含对实际列表的引用 对象a被绑定到,并且函数g()获取对该单元格的引用。 。这样,当函数f()退出时,列表对象和单元格保持有效,即使是 。

+0

非常感谢您的解释,您的编辑使事情变得非常清楚。我想知道,为了学习(C)Python的内部机制,是否可以通过检查或类似的方式访问您在最后一段中提到的“单元格”? – rahmu 2012-08-06 15:27:58

+0

@rahmu:我解释了一个错误(不会变化太大)。不幸的是,这些单元对于Python代码来说是完全透明的,并且总是被它们引用的对象替换,所以它们不能被检查。 – 2012-08-06 16:13:08

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 
>>> 

为了记录在案,你想acheive所谓Memoization,并有可从中做同样的事情的decorator pattern page更完整memoizing装饰,但使用装饰者类别。你的代码和基于类的修饰器基本上是一样的,基于类的修饰器在存储之前检查散列能力。


编辑(2017年2月2日):@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理解上面的代码是什么意思。

+0

这是如何工作的Python装饰?每次我调用'cached(foo)(2)'时,它都不会缓存结果并休眠两秒钟。装饰函数的每次调用是否引用同一个装饰器? – 2017-02-02 08:19:49

+0

@SiminJie - 查看我对答案的额外编辑。 – brice 2017-02-02 17:23:02