Python class 中的 LRU 缓存在使用静态方法或 class 方法装饰器装饰时忽略最大大小限制

Python LRU cache in a class disregards maxsize limit when decorated with a staticmethod or classmethod decorator

我正在查看 Python 的 LRU 缓存装饰器的实现 details,并注意到这种行为,我觉得有点惊讶。当用 staticmethodclassmethod 装饰器装饰时,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

我期待它——

运行 上面带有实例方法的代码的行为方式与我在要点中解释的方式相同。

使用缓存信息检查 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 的缓存信息对于 foofoo1 实例都是相同的——这是预期的——但是 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()