Best practice for reseting `@functools.cached_property`

Consider the following:

import functools

class C:
    @functools.cached_property
    def foo(self):
        print('expensive calc')
        return 42

    def reset(self):
        del self.foo

The following works as expected:

c = C()
print(c.foo)
print(c.foo)
c.reset()
print(c.foo)

giving:

$ ./u.py
expensive calc
42
42
expensive calc
42

But, if the del is invoked before the property is created (or, equivalently, twice in a row), then we see an AttributeError:

$ ./u.py
expensive calc
42
42
expensive calc
42
Traceback (most recent call last):
  File "/home/nexus/./u.py", line 20, in <module>
    c.reset()
  File "/home/nexus/./u.py", line 12, in reset
    del self.foo
        ^^^^^^^^
AttributeError: 'C' object has no attribute 'foo'

Is there an official way to detect that the property is currently not present, so deleting it makes no sense, or is wrapping it in a try/except AttributeError the correct approach?

And, for either solution, should it be mentioned on functools — Higher-order functions and operations on callable objects — Python 3.13.1 documentation along with:

The cached value can be cleared by deleting the attribute. This allows the cached_property method to run again.

Sorry I don’t know the answer for certain. To be honest I would consider it best practice not to use @cached_property in a situation where you need to reset it. That sounds like it’s asking for bugs. I’m curious why you need this. Could you instead use a pattern like

class C:
  @property
  def foo(self):
     return _f(self.maybe_changed_attributes) # or self._m(self.maybe_changed_attributes)

@lru_cache(1)
def _f(...):
  print('expensive calc')
  return 42

?

Yeah, catching an AttributeError seems reasonable. You could use hasattr to check if you really wanted to. Finally, you could also mutate the instance’s __dict__ e.g. via pop("foo", None)

That seems to avoid the error not by avoiding the del but by creating the attribute so that the del then succeeds:

import functools

class C:
    @functools.cached_property
    def foo(self):
        print('expensive calc')
        return 42

    def reset(self):
        if hasattr(self, 'foo'):
           del self.foo

c = C()
c.reset()
c.reset()
print(c.foo)

Output (Attempt This Online!):

expensive calc
expensive calc
expensive calc
42

Should be only one expensive calc, not three.

2 Likes

The main app doesn’t need it because it is usually a singleton that gets passed around (database connection).

But, while experimenting with resetting during tests was when I discovered this.

1 Like

hasattr was one of the approaches I tried. In retrospect, I should have mentioned that in my original message.

Given the documentation,

When it does run, the cached_property writes to the attribute with the same name.

(i.e., an instance attribute that shadows the class attribute to which the cached_property is bound)

and

Also, this decorator requires that the __dict__ attribute on each instance be a mutable mapping.

I’d suggest replacing hasattr with a direct lookup in __dict__:

def reset(self):
    if 'foo' in self.__dict__:
        del self.foo  # or del self.__dict__['foo']

Doh!

Out of habit, I kept using dir(c) instead of c.__dict__, and foo was always present. In practice, I have not had a need to use __dict__ in years, and maybe then, probably only once in some random library, and forgot that the two are not the same.

It wasn’t until it was mentioned so many times in the discussion did I realized I missing something so fundamental.

Thanks!

Is that better than the unconditional self.__dict__.pop("foo", None) suggested earlier?

I missed that suggestion.

I would imagine it would depend on context.

If more than removing the entry is necessary, then if ... in is probably more appropriate.

Otherwise, they are likely equivalent and comes down to a style question.

Personally, I would opt for if ... in for the same reason I always use if (...) { stuff } instead of if (...) stuff in brace-style languages. I may not need more than one line today, but I might tomorrow, so let’s stay consistent. E.g., my “style”.