使用 staticmethod 或 classmethod 装饰器装饰时,类中的 Python LRU 缓存忽略最大大小限制

Posted

技术标签:

【中文标题】使用 staticmethod 或 classmethod 装饰器装饰时,类中的 Python LRU 缓存忽略最大大小限制【英文标题】:Python LRU cache in a class disregards maxsize limit when decorated with a staticmethod or classmethod decorator 【发布时间】:2022-01-21 08:29:00 【问题描述】:

我正在查看 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 缓存装饰器将为类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 次。 然后再次等待 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)

这怎么可能? foofoo1 实例的名为 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 参数,对吗?您介意将其添加为我可以接受的答案吗?非常感谢! 【参考方案1】:

正如您在编辑中所说,@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()

【讨论】:

以上是关于使用 staticmethod 或 classmethod 装饰器装饰时,类中的 Python LRU 缓存忽略最大大小限制的主要内容,如果未能解决你的问题,请参考以下文章

当我需要在 python 编程中使用@staticmethod 和@classmethod 时? [复制]

@staticmethod与@classmethod的异同点

类的静态方法@staticmethod

@staticmethod和@classmethod的作用与区别

python中静态方法(@staticmethod)和类方法(@classmethod)的区别

静态方法@staticmethod 属性方法@property