When an object is used as an argument to a @lru_cache-decorated function, the object becomes part of the internal cache key. This cache key is stored in 2 places, once by reference as the link.key in one of the nodes of the linked list starting at self.__root, and once in the dictionary at self.__map() (where it is keyed based on hash).
If the hash() of this object subsequently changes, we will encounter a bug when we attempt to evict that entry from the cache, as we can still find the object by reference in the linked-list, but when we try and delete it from self.__map, we get a KeyError because its hash has changed and the object in the dictionary is stored under the original hash. The code in question is here
Here is a simple script to demostrate the issue.
import sys
if sys.version_info.major == 3:
from functools import lru_cache
else:
from functools32 import lru_cache
@lru_cache(maxsize=1)
def foo(arg):
return "return value"
class Hashable(object):
def __init__(self, value):
self.value = value
def __hash__(self):
return self.value
h1 = Hashable(1)
foo(h1) # add h1 to cache
h1.value = 3 # but then change its hash value
h2 = Hashable(2)
foo(h2) # evicts h1
Works in python3/functools, fails with KeyError: (<__main__.Hashable object at 0x10d9daf10>,) line 132, in popitem in python2/functools32
One might argue that having objects change their hash is a terrible thing to do, but unfortunately a lot of objects do have this behavior. Django models for example. Their hash is based off their primary key, so it changes when you save() the model instance.
class MyModel(Model):
@lru_cache(maxsize=1)
def get_value(self):
...
mm = MyModel()
value = mm.lru_cached_method()
mm.save()
# Later if we try and evict this result of `get_value()` it will error, as the hash of `mm` has changed
When an object is used as an argument to a
@lru_cache-decorated function, the object becomes part of the internal cache key. This cache key is stored in 2 places, once by reference as thelink.keyin one of the nodes of the linked list starting atself.__root, and once in the dictionary atself.__map()(where it is keyed based on hash).If the
hash()of this object subsequently changes, we will encounter a bug when we attempt to evict that entry from the cache, as we can still find the object by reference in the linked-list, but when we try and delete it fromself.__map, we get a KeyError because its hash has changed and the object in the dictionary is stored under the original hash. The code in question is hereHere is a simple script to demostrate the issue.
Works in python3/functools, fails with
KeyError: (<__main__.Hashable object at 0x10d9daf10>,) line 132, in popitemin python2/functools32One might argue that having objects change their hash is a terrible thing to do, but unfortunately a lot of objects do have this behavior. Django models for example. Their hash is based off their primary key, so it changes when you
save()the model instance.