我有一个简单的小装饰器,它将函数调用的结果缓存在dict
中作为函数属性。
from decorator import decorator
def _dynamic_programming(f, *args, **kwargs):
try:
f.cache[args]
except KeyError:
f.cache[args] = f(*args, **kwargs)
return f.cache[args]
def dynamic_programming(f):
f.cache = {}
return decorator(_dynamic_programming, f)
我现在想添加清空缓存的可能性。所以我把dynamic_programming()
函数改成这样:
def dynamic_programming(f):
f.cache = {}
def clear():
f.cache = {}
f.clear = clear
return decorator(_dynamic_programming, f)
现在让我们假设我用这个小东西来实现斐波那契数函数:
@dynamic_programming
def fib(n):
if n <= 1:
return 1
else:
return fib(n-1) + fib(n-2)
>>> fib(4)
5
>>> fib.cache
{(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5}
但现在,当我清除缓存时,发生了一些奇怪的事情:
>>> fib.clear()
>>> fib.cache
{(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5}
或者(运行一个新的Python内核)用另一种方式:
>>> fib.clear()
>>> fib(4)
5
>>> fib.cache
{}
为什么缓存在第一次访问后无法"访问",即在调用后调用clear()
或在clear()
后调用时没有更改?
(顺便说一句,我知道一个正确清除缓存的解决方案:调用f.cache.clear()
而不是将{}
分配给它,可以按预期工作。我只对分配解决方案失败的原因感兴趣。)
问题出在decorator
模块上。如果您在装饰器中添加一些print
语句:
from decorator import decorator
def _dynamic_programming(f, *args, **kwargs):
print "Inside decorator", id(f.cache)
try:
f.cache[args]
except KeyError:
f.cache[args] = f(*args, **kwargs)
return f.cache[args]
def dynamic_programming(f):
f.cache = {}
print "Original cache", id(f.cache)
def clear():
f.cache = {}
print "New cache", id(f.cache)
f.clear = clear
return decorator(_dynamic_programming, f)
@dynamic_programming
def fib(n):
if n <= 1:
return 1
else:
return fib(n-1) + fib(n-2)
print fib(4)
print id(fib.cache)
fib.clear()
print id(fib.cache)
print fib(10)
print id(fib.cache)
它输出(跳过重复行):
Original cache 139877501744024
Inside decorator 139877501744024
5
139877501744024
New cache 139877501802208
139877501744024
Inside decorator 139877501802208
89
139877501744024
正如您所看到的,decorator中的cache
会根据clear函数进行更改。但是,从__main__
访问的cache
没有改变。在装饰器内外打印cache
可以得到更清晰的图像(同样,跳过重复):
Inside decorator {}
Inside decorator {(1,): 1}
Inside decorator {(2,): 2, (0,): 1, (1,): 1}
Inside decorator {(2,): 2, (0,): 1, (3,): 3, (1,): 1}
5
Outside {(2,): 2, (0,): 1, (3,): 3, (1,): 1, (4,): 5}
Inside decorator {}
Inside decorator {(1,): 1}
Inside decorator {(2,): 2, (0,): 1, (1,): 1}
Inside decorator {(2,): 2, (0,): 1, (3,): 3, (1,): 1}
Inside decorator {(2,): 2, (0,): 1, (3,): 3, (1,): 1, (4,): 5}
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5, (5,): 8}
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5, (5,): 8, (6,): 13}
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5, (5,): 8, (6,): 13, (7,): 21}
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (8,): 34, (3,): 3, (4,): 5, (5,): 8, (6,): 13, (7,): 21}
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (8,): 34, (3,): 3, (9,): 55, (4,): 5, (5,): 8, (6,): 13, (7,): 21}
89
Outside {(2,): 2, (0,): 1, (3,): 3, (1,): 1, (4,): 5}
正如你所看到的,内部的变化并没有在外部得到回应。问题是,在decorator
模块中,有一行(在用于制作装饰器的类中):
self.dict = func.__dict__.copy()
然后:
func.__dict__ = getattr(self, 'dict', {})
所以基本上,外面的__dict__
和里面的__dict__
是不同的。这意味着:
__dict__
由装饰器复制(未引用)- 当
cache
发生变化时,它会改变内部__dict__
,而不是外部__dict__
- 因此,
_dynamic_programming
使用的cache
被清除,但您无法从外部看到这一点,因为装饰器的__dict__
仍然指向旧的cache
(正如您在上面看到的,因为内部cache
更新,而外部cache
保持不变)
总之,这是decorator
模块的一个问题。
所以@matsjoyce的回答非常有趣和深入,我知道你已经有了解决方案,但我总是发现写自己的装饰器会更清楚一点:
def dynamic_programming(f):
def wrapper(*args, **kwargs):
try:
return wrapper.cache[args]
except KeyError:
res = wrapper.cache[args] = f(*args, **kwargs)
return res
wrapper.cache = {}
wrapper.clear = wrapper.cache.clear
return wrapper