How do I compute an expensive attribute only once in Python?
Short answer
Use functools.cached_property so the value is computed on first access and stored on the instance. Unlike lru_cache it does not hold the object alive, but it never recomputes, so only use it for values that do not change.
The short version
Decorate the method with functools.cached_property. It is computed on first access, stored on the instance, and returned directly every time after. It suits a value that is expensive to derive and does not change for the life of the object.
Using it
from functools import cached_property
class Report:
def __init__(self, rows):
self.rows = rows
@cached_property
def total(self):
print("computing")
return sum(self.rows)
r = Report([1, 2, 3])
r.total # prints "computing", returns 6
r.total # returns 6 immediately, no print
The first access does the work; every later access reads the stored value. Note it is accessed as an attribute (r.total), not called as a method.
Compared with a plain property
A plain @property recomputes on every single access, which is fine for something cheap and essential for something that must reflect current state. If the computation is expensive and the inputs do not change, that repetition is pure waste, especially when the property is read inside a loop.
The staleness danger
This is the catch, and it is a real one. The cached value never updates. If you change the underlying data, the property keeps returning the old answer.
r.rows.append(4)
r.total # still 6, not 10
Nothing warns you. So only use cached_property on objects that are effectively immutable after construction, or be disciplined about clearing the cache when the inputs change.
del r.total # removes the cached value, so the next access recomputes
Compared with lru_cache
lru_cache on a method also caches, but it keys the cache on the arguments including self, which keeps the instance alive in the cache and can leak memory. cached_property stores the value on the instance itself, so it is freed when the object is, which is the correct behaviour for per-instance values.
Use lru_cache for pure functions of their arguments, and cached_property for a derived attribute of one object.
A pandas example
The pattern fits an analysis object that derives several expensive summaries from one frame.
class Analysis:
def __init__(self, df):
self.df = df
@cached_property
def by_city(self):
return self.df.groupby("city")["amount"].sum()
Several methods can then use self.by_city without each triggering a fresh groupby, and the aggregation runs once.
Only for values that do not change
That is the whole rule. If the object is a snapshot of data being analysed, caching is safe and valuable. If the object is mutable and the property must track its state, use a plain property and accept the cost, because a stale cached answer is far worse than a slow correct one.