Automatically clearing/invalidating the lru cache
pbsds opened this issue · comments
lru_cache
introduces a correctness problem for methods or properties whose output depend on attributes stored in self
.
Of course you can manually call clear_cache
, but
- this is a leaky abstraction
- this method is highly inaccessible for properties
Here i explore how the cache behaves, along with my suggested solution:
problem exploration
from methodtools import lru_cache
# no magic here
class Foo:
a = 1
b = 2
@lru_cache()
@property
def prop(self):
return self.a + self.b
@lru_cache()
def method(self):
return self.a + self.b
# prints __setattr__ events
class Bar(Foo):
def __setattr__(self, key, value):
print(f"{self.__class__.__name__}.__setattr__({key!r}, {value!r})")
return super().__setattr__(key, value)
# clears its cache on relevant __setattr__ events
class Baz(Foo):
def __setattr__(self, key, value):
print(f"{self.__class__.__name__}.__setattr__({key!r}, {value!r})")
if not key.startswith("__wire|"):
for attr in dir(self):
if attr.startswith("__wire|"):
getattr(self, attr).cache_clear()
return super().__setattr__(key, value)
# tests
print("-"*10)
for i in (Foo, Bar, Baz):
obj = i()
print(f"{obj.prop = }, {obj.method() = }")
obj.a = 4
print(f"{obj.prop = }, {obj.method() = }")
getattr(obj, "__wire|Foo|prop").cache_clear()
print(f"{obj.prop = }, {obj.method() = }")
print("-"*10)
output
----------
obj.prop = 3, obj.method() = 3
obj.prop = 3, obj.method() = 3
obj.prop = 6, obj.method() = 3
----------
Bar.__setattr__('__wire|Foo|prop', <methodtools._LruCacheWire object at 0x7f23675248e0>)
Bar.__setattr__('__wire|Foo|method', <methodtools._LruCacheWire object at 0x7f2367524940>)
obj.prop = 3, obj.method() = 3
Bar.__setattr__('a', 4)
obj.prop = 3, obj.method() = 3
obj.prop = 6, obj.method() = 3
----------
Baz.__setattr__('__wire|Foo|prop', <methodtools._LruCacheWire object at 0x7f23675249a0>)
Baz.__setattr__('__wire|Foo|method', <methodtools._LruCacheWire object at 0x7f2367524a00>)
obj.prop = 3, obj.method() = 3
Baz.__setattr__('a', 4)
obj.prop = 6, obj.method() = 6
obj.prop = 6, obj.method() = 6
----------
My suggested solution is an optional mixin class, that can be added as a parent class:
class InvalidateLRUOnWriteMixin:
def __setattr__(self, key, value):
if not key.startswith("__wire|"):
for attr in dir(self):
if attr.startswith("__wire|"):
getattr(self, attr).cache_clear()
return super().__setattr__(key, value)
It clears all caches each time you write to an attribute stored in the class.
Although a pessimistic approach, it does work.
Do you want a PR? Comments?