I was going through the implementation details of Python's LRU cache decorator and noticed this behavior that I found a bit surprising. When decorated with either staticmethod
or classmethod
decorator, lru_cache
disregards the maxsize
limit. Consider this example:
# 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()
From the implementation, it's clear to me that using the LRU cache decorator in this way will create a shared cache for all the instances of class Foo
. However, when I run the code, it waits for 3 seconds in the beginning and then prints out 15
six times without pausing in the middle.
$ python src.py
# Waits for three seconds and then prints out 15 six times
15
15
15
15
15
15
I was expecting it to—
- wait for 3 seconds
- then print
15
three times - then wait again for 3 seconds
- and finally, print
15
three times
Running the above code with an instance method behaves in the way that I've explained in the bullet points.
Inspecting the foo.bar
method with cache info gives me the following result:
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)
How is this possible? The cache info named tuple is the same for both foo
and foo1
instance—which is expected—but how come the LRU cache is behaving as if it was applied as lru_cache(None)(func)
. Is this because of descriptor intervention or something else? Why is it disregarding the cache limit? Why does running the code with an instance method works as explained above?
Edit: As Klaus mentioned in the comment that this is caching 3 keys, not 3 accesses. So, for a key to be evicted, the method needs to be called at least 4 times with different arguments. That explains why it's quickly printing 15
six times without pausing in the middle. It's not exactly disregarding the maxlimit.
Also, in the case of an instance method, lru_cache
utilizes the self
argument to hash and build the key for each argument in the cache dictionary. So each new instance method will have a different key for the same argument due to the inclusion of self
in the hash calculation. For static methods, there's no self
argument and for class methods, the cls
is the same class in different instances. This explains the differences in their behaviors.
CodePudding user response:
Like you said in your edit, the @staticmethod
and @classmethod
decorators will make the cache be shared among all instances.
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()