【发布时间】:2022-01-21 08:29:00
【问题描述】:
我正在查看 Python 的 LRU 缓存装饰器的实现 details,并注意到这种行为让我有点惊讶。当使用 staticmethod 或 classmethod 装饰器进行装饰时,lru_cache 会忽略 maxsize 限制。考虑这个例子:
# src.py
import time
from functools import lru_cache
class Foo:
@staticmethod
@lru_cache(3)
def bar(x):
time.sleep(3)
return x + 5
def main():
foo = Foo()
print(foo.bar(10))
print(foo.bar(10))
print(foo.bar(10))
foo1 = Foo()
print(foo1.bar(10))
print(foo1.bar(10))
print(foo1.bar(10))
if __name__ == "__main__":
main()
从实现中,我很清楚,以这种方式使用 LRU 缓存装饰器将为类Foo 的所有实例创建一个共享缓存。但是,当我运行代码时,它会在开始时等待 3 秒,然后打印出 15 六次,中间没有暂停。
$ python src.py
# Waits for three seconds and then prints out 15 six times
15
15
15
15
15
15
我期待它——
- 等待 3 秒。
- 然后打印
153 次。 - 然后再次等待 3 秒。
- 最后,打印三遍
15。
使用实例方法运行上述代码的行为方式与我在要点中解释的方式相同。
使用缓存信息检查 foo.bar 方法会得到以下结果:
print(f"{foo.bar.cache_info()=}")
print(f"{foo1.bar.cache_info()=}")
foo.bar.cache_info()=CacheInfo(hits=5, misses=1, maxsize=3, currsize=1)
foo1.bar.cache_info()=CacheInfo(hits=5, misses=1, maxsize=3, currsize=1)
这怎么可能? foo 和 foo1 实例的名为 tuple 的缓存信息是相同的——这是意料之中的——但是 LRU 缓存为什么表现得好像它被应用为 lru_cache(None)(func)。这是因为描述符干预还是其他原因?为什么不考虑缓存限制?为什么使用实例方法运行代码会像上面解释的那样工作?
编辑: 正如 Klaus 在评论中提到的,这是缓存 3 个密钥,而不是 3 个访问。因此,要驱逐一个键,需要使用不同的参数调用该方法至少 4 次。这就解释了为什么它会快速打印15 六次而不会在中间暂停。它并没有完全忽略最大限制。
此外,在实例方法的情况下,lru_cache 使用self 参数对缓存字典中的每个参数进行散列并构建键。因此,由于在哈希计算中包含self,每个新的实例方法对于相同的参数都有不同的键。对于静态方法,没有 self 参数,对于类方法,cls 是不同实例中的同一个类。这解释了他们行为上的差异。
【问题讨论】:
-
像这样,您正在缓存 3 个密钥,而不是 3 个访问。要删除一个键,您至少需要 4 个不同的调用。
-
嗯..在那种情况下,为什么它对实例方法的作用不同?在实例方法的情况下,运行上面的代码等待 3 秒,然后打印 15 三次,然后再次等待 3 秒,最后打印 15 三次。我还是觉得这有点奇怪。
-
实例是调用的一部分。新实例,新缓存键。
-
我明白了。所以基本上它在缓存的散列键中使用
self参数,对吗?您介意将其添加为我可以接受的答案吗?非常感谢!
标签: python python-3.x caching lru