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函数返回后变量仍然存在吗?

4

2 回答 2

12

您在这里创建了一个闭包:该函数从封闭范围内_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()被调用时会发生什么?第一行创建一个新的列表对象并将其绑定到 name a。下一行创建一个新的函数对象(使用在模块编译期间创建的代码对象)并将其绑定到 name gg()此时不执行主体,最后返回函数对象。

由于 的代码对象f()有一个注释,该名称a是由本地函数引用的,因此在输入时会为该名称创建一个“单元格” f()。此单元格包含对绑定到的实际列表对象的引用a,并且该函数g()获取对此单元格的引用。这样,即使函数f()退出,列表对象和单元格也会保持活动状态。

于 2012-08-06T14:52:02.687 回答
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,装饰器模式页面提供了一个更完整的记忆装饰器,它做同样的事情,但使用了一个装饰器。您的代码和基于类的装饰器本质上是相同的,基于类的装饰器在存储之前会检查哈希能力。


编辑(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

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

于 2012-08-06T15:13:41.010 回答