52

I am currently using @cached_property on a model class and I would like to delete it on save so that it can be repopulated on the next call. How do I do this? Example:

class Amodel():
    #...model_fields....

    @cached_property
    def db_connection(self):
        #get some thing in the db and cache here


instance = Amodel.objects.get(id=1)
variable = instance.db_connection

Amodel.objects.select_for_update().filter(id=1).update(#some variable)
#invalidate instance.db_connection
#new_variable = instance.db_connection

Thanks

Uri
  • 2,992
  • 8
  • 43
  • 86
user1711168
  • 677
  • 1
  • 6
  • 12

4 Answers4

83

Just del it as documentation says. It will lead to recalculation on next access.

class SomeClass(object):
    
    @cached_property
    def expensive_property(self):
         return datetime.now()

obj = SomeClass()
print obj.expensive_property
print obj.expensive_property # outputs the same value as before
del obj.expensive_property
print obj.expensive_property # outputs new value

For Python 3 it's the same use of del. Below is an example of a try/except block.

try:
    del obj.expensive_property 
except AttributeError:
    pass 
Antoine Pinsard
  • 33,148
  • 8
  • 67
  • 87
isobolev
  • 1,473
  • 20
  • 19
  • 29
    note this will generate an AttributeError if the property has not been accessed/cached. In that case wrap it with a try,except AttributeError. – dalore Nov 04 '14 at 14:31
  • 4
    Good info @dalore; terrible boilerplate on the part of the django devs :( – DylanYoung Jul 24 '16 at 18:31
  • 4
    Note that if you are invalidating from within the class, you would use `del self.__dict__['expensive_property']`...unless there's a non-dunder way that I'm unaware of. – Tom Aug 18 '16 at 16:01
  • 3
    `@cached_property` is most definitely *not* a bit of terrible boilerplate! It's a concise use of the interplay between instance dictionaries and the descriptor protocol. It makes perfect sense that if you try and `delattr` before the property has been called/cached then it will throw `AttributeError`, that's bog-standard python behaviour. And it makes perfect sense because `cached_property` precisely *writes* the attribute to the instance dict in lieu of calling itself after the first time it is run. Take the time to look into it and you'll see that it's a sweet and economical nugget of code. – jhrr Jan 08 '18 at 03:43
  • Link changed to https://docs.djangoproject.com/en/3.0/ref/utils/#django.utils.functional.cached_property; can't edit. – Uri May 13 '20 at 17:31
  • @jhrr No problem with `cached_property`. It's the error-pattern that's suspect in this case, but as you say it's probably just done to reduce code because that's how python works. In the case of a cached property it doesn't really make sense though, as the property that's being `del`'d from a user perspective *does* exist. It's a leaky abstraction. – DylanYoung May 06 '22 at 15:53
  • Though it'd be a fair point to say that the use of `del` itself is the design problem there. – DylanYoung May 06 '22 at 17:28
18

I created a Django model mixin that invalidates all @cached_property properties on the model when model.refresh_from_db() is called. You can also manually invalidate the cached properties with model.invalidate_cached_properties().

from django.utils.functional import cached_property


class InvalidateCachedPropertiesMixin():

    def refresh_from_db(self, *args, **kwargs):
        self.invalidate_cached_properties()
        return super().refresh_from_db(*args, **kwargs)
            
    def invalidate_cached_properties(self):
        for key, value in self.__class__.__dict__.items():
            if isinstance(value, cached_property):
                self.__dict__.pop(key, None)

https://gitlab.com/snippets/1747035

Inspired by Thomas Baden's answer.

gitaarik
  • 42,736
  • 12
  • 98
  • 105
  • 3
    If you using `from functools import cached_property` it will not work. So I suggest, check multiple field types: `if isinstance(value, (cached_property, django_cached_property)):` – Kubas Apr 16 '20 at 05:12
  • 1
    In case anyone else runs into this - this won't clear properties defined on a superclass. To do that you have to loop through every class in `self.__class__.__mro__` at the beginning of `invalidate_cached_properties`. Have also left a comment with the complete code on the gitlab link. – Cory Dec 12 '22 at 15:53
8

Edited heavily due to ongoing development... Now supports multiple tags for a given cached_property.

I encountered a similar issue, wherein I had a set of related cached_property objects which all needed simultaneous invalidation. I solved it in this manner:

  1. Extend cached_property to accept tag values and include a decorator classmethod:

    def __init__(self, func, *tags):
        self.func = func
        self.tags = frozenset(tags)
    
    @classmethod
    def tag(cls *tags):
        return lambda f: cls(f, *tags)
    
  2. In my other objects, use my new cached_property.tag decorator classmethod to define tagged cached_property methods:

    @cached_property.tag("foo_group")
    def foo(self):
        return "foo"
    
  3. On my object that makes use of the new decorator, write a method to invalidate all cached_property values with the named tag by walking the __dict__ of the instantiated object's class. This prevents accidental invocation of all cached_property methods:

    def invalidate(self, tag):
        for key, value in self.__class__.__dict__.items():
            if isinstance(value, cached_property) and tag in value.tags:
                self.__dict__.pop(key, None)
    

Now, to invalidate, I merely invoke myobject.invalidate("foo_group").

6

If you don't want to use try and except, and also write fewer lines, you can use:

if ("expensive_property" in obj.__dict__):
    del obj.expensive_property

Or:

if ("expensive_property" in obj.__dict__):
    delattr(obj, "expensive_property")

It will delete the cached property and it will be calculated again the next time it's accessed.

Update: Don't use if (hasattr(obj, "expensive_property")):! It will calculate the property if it's not cached already and will always return True!

Uri
  • 2,992
  • 8
  • 43
  • 86