cached_property

functools · Practical · 9 min

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.stats to 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 2

Remember

  • @cached_property computes 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 @property stacked on @lru_cache for per-instance caching, which leaks instances.

Check yourself

  1. Why is a cached_property as fast as a plain attribute after the first access?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/functools.html#functools.cached_property