Python class 中的 LRU 缓存在使用静态方法或 class 方法装饰器装饰时忽略最大大小限制
Python LRU cache in a class disregards maxsize limit when decorated with a staticmethod or classmethod decorator
我正在查看 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 缓存装饰器将为 class Foo
的所有实例创建一个共享缓存。但是,当我 运行 代码时,它在开始时等待 3 秒,然后打印出 15
六次,中间没有暂停。
$ python src.py
# Waits for three seconds and then prints out 15 six times
15
15
15
15
15
15
我期待它——
- 等待 3 秒。
- 然后打印
15
三次。
- 然后再等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)
这怎么可能?名为 tuple 的缓存信息对于 foo
和 foo1
实例都是相同的——这是预期的——但是 LRU 缓存怎么表现得好像它被应用为 lru_cache(None)(func)
。这是因为描述符干预还是其他原因?为什么它无视缓存限制?为什么 运行 将代码与实例方法结合起来会像上面解释的那样工作?
编辑: 正如 Klaus 在评论中提到的,这是缓存 3 个键,而不是 3 个访问。因此,对于要逐出的密钥,需要使用不同的参数至少调用该方法 4 次。这就解释了为什么它快速打印 15
六次而中间没有停顿。这并不是完全无视最大限制。
此外,在实例方法的情况下,lru_cache
利用 self
参数对缓存字典中的每个参数进行散列和构建键。因此,由于在哈希计算中包含 self
,因此每个新的实例方法对于相同的参数将具有不同的键。对于静态方法,没有 self
参数,对于 class 方法,cls
在不同实例中是相同的 class。这解释了他们行为上的差异。
正如您在编辑中所说,@staticmethod
和 @classmethod
装饰器将使缓存在所有实例之间共享。
import time
from functools import lru_cache
class Foo:
@staticmethod
@lru_cache(3)
def foo(x):
time.sleep(1)
return x + 5
class Bar:
@lru_cache(3)
def bar(self, x):
time.sleep(1)
return x + 5
def main():
# fill the shared cache takes around 3 seconds
foo0 = Foo()
print(foo0.foo(10), foo0.foo(11), foo0.foo(12))
# get from the shared cache takes very little time
foo1 = Foo()
print(foo1.foo(10), foo1.foo(11), foo1.foo(12))
# fill the instance 0 cache takes around 3 seconds
bar0 = Bar()
print(bar0.bar(10), bar0.bar(11), bar0.bar(12))
# fill the instance 1 cache takes around 3 seconds again
bar1 = Bar()
print(bar1.bar(10), bar1.bar(11), bar1.bar(12))
if __name__ == "__main__":
main()
我正在查看 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 缓存装饰器将为 class Foo
的所有实例创建一个共享缓存。但是,当我 运行 代码时,它在开始时等待 3 秒,然后打印出 15
六次,中间没有暂停。
$ python src.py
# Waits for three seconds and then prints out 15 six times
15
15
15
15
15
15
我期待它——
- 等待 3 秒。
- 然后打印
15
三次。 - 然后再等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)
这怎么可能?名为 tuple 的缓存信息对于 foo
和 foo1
实例都是相同的——这是预期的——但是 LRU 缓存怎么表现得好像它被应用为 lru_cache(None)(func)
。这是因为描述符干预还是其他原因?为什么它无视缓存限制?为什么 运行 将代码与实例方法结合起来会像上面解释的那样工作?
编辑: 正如 Klaus 在评论中提到的,这是缓存 3 个键,而不是 3 个访问。因此,对于要逐出的密钥,需要使用不同的参数至少调用该方法 4 次。这就解释了为什么它快速打印 15
六次而中间没有停顿。这并不是完全无视最大限制。
此外,在实例方法的情况下,lru_cache
利用 self
参数对缓存字典中的每个参数进行散列和构建键。因此,由于在哈希计算中包含 self
,因此每个新的实例方法对于相同的参数将具有不同的键。对于静态方法,没有 self
参数,对于 class 方法,cls
在不同实例中是相同的 class。这解释了他们行为上的差异。
正如您在编辑中所说,@staticmethod
和 @classmethod
装饰器将使缓存在所有实例之间共享。
import time
from functools import lru_cache
class Foo:
@staticmethod
@lru_cache(3)
def foo(x):
time.sleep(1)
return x + 5
class Bar:
@lru_cache(3)
def bar(self, x):
time.sleep(1)
return x + 5
def main():
# fill the shared cache takes around 3 seconds
foo0 = Foo()
print(foo0.foo(10), foo0.foo(11), foo0.foo(12))
# get from the shared cache takes very little time
foo1 = Foo()
print(foo1.foo(10), foo1.foo(11), foo1.foo(12))
# fill the instance 0 cache takes around 3 seconds
bar0 = Bar()
print(bar0.bar(10), bar0.bar(11), bar0.bar(12))
# fill the instance 1 cache takes around 3 seconds again
bar1 = Bar()
print(bar1.bar(10), bar1.bar(11), bar1.bar(12))
if __name__ == "__main__":
main()