以下函数用作存储已计算值结果的装饰器。如果之前已经计算过该参数,该函数将返回存储在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
变量仍然存在吗?
答案 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以了解上述代码的含义。