Skip to content

← Everyday Standard Library Tools step 29 of 55

Medium Primitives

Cache a Report Total Until You Clear It

The starter caches each report's total in a dictionary on the class, with the report as the key. That dictionary keeps every report alive after the caller has finished with it. Rewrite Report to store the total on the instance with cached_property.

report = Report([1, 2, 3])
report.total         # 6, computed
report.rows.append(5)
report.total         # 6, still cached
report.invalidate()
report.total         # 11, computed again
report.computations  # 2

What to write

Fill in Report(rows):

  • Copy the input into self.rows, so that appending to it does not change the caller's list.
  • Start self.computations at 0.
  • Make total a cached_property. Each time its method runs, add 1 to computations and return sum(self.rows). The decorator caches a total of 0 like any other value.
  • Give Report an invalidate() method that removes a stored total. Calling it before the first read, or twice in a row, must not raise and must not compute a total.
  • Remove the class-level dictionary. Keep Report a normal class without __slots__, because cached_property needs the instance's __dict__.

What the report shows

Keep solve as it is. It creates one Report from rows and runs each operation in script, in order:

Operation Effect
read Appends report.total to reads
invalidate Calls report.invalidate()
add Appends 5 to report.rows, without invalidating

Any other operation raises ValueError. Then solve saves computations, makes a weak reference to the report, deletes its own reference, and runs gc.collect(). A weak reference lets you check whether an object still exists without keeping it alive. The result is:

{"reads": reads, "computations": computations, "leaked": tracker() is not None}

The tests expect leaked to be False. With the starter it is True whenever a total is still cached when the script ends, because the class-level dictionary then still holds the report.

For rows=[1, 2, 3] and script=["read", "add", "read", "invalidate", "read"], the result is {"reads": [6, 6, 11], "computations": 2, "leaked": False}.

Edge cases

  • Empty rows give a total of 0.
  • An empty script gives reads == [] and computations == 0.
  • Invalidating does not reset computations.