Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically clearing/invalidating the lru cache #16

Open
pbsds opened this issue Jun 9, 2022 · 0 comments
Open

Automatically clearing/invalidating the lru cache #16

pbsds opened this issue Jun 9, 2022 · 0 comments

Comments

@pbsds
Copy link

pbsds commented Jun 9, 2022

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

  1. this is a leaky abstraction
  2. 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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant