Lazily computed, per-object values like parsed data or derived statistics are common in classes.
@cached_property turns a method into an attribute that is computed on first access and then stored on the instance:
class Report:
@cached_property
def stats(self):
return expensive_analysis(self.rows)The first report.stats runs the method and writes the result into report.__dict__["stats"]. Because cached_property is a non-data descriptor, later lookups find the instance attribute first and never call the descriptor again, so access is as fast as a normal attribute.
Consequences:
- The cache is per instance and lives as long as the instance. Delete it with
del obj.statsto force recomputation, for example after the inputs change. - The attribute is writable: assigning
obj.stats = ...simply replaces the cached value. - It needs an instance
__dict__; classes using__slots__without__dict__can't use it. - Since Python 3.12 it no longer holds a lock, so two threads may compute the value concurrently on first access. Add your own lock if the computation must run once.
Compare with @property + @cache: that combination caches on the class-level function keyed by self, keeps every instance alive forever and requires hashable instances. cached_property avoids all of that.
Example
from functools import cached_property
class Dataset:
def __init__(self, values):
self.values = values
self.computations = 0
@cached_property
def mean(self):
self.computations += 1
return sum(self.values) / len(self.values)
d = Dataset([2, 4, 6])
print(d.mean, d.mean, d.computations) # 4.0 4.0 1
print("mean" in vars(d)) # True: stored on the instance
d.values.append(8)
print(d.mean) # 4.0 (stale!)
del d.mean # invalidate
print(d.mean, d.computations) # 5.0 2Remember
@cached_propertycomputes once per instance and stores the value in__dict__- Invalidate with
del obj.attr; the value goes stale if inputs change - No lock since 3.12; requires a
__dict__
Watch out for
- Using
@propertystacked on@lru_cachefor per-instance caching, which leaks instances.