Problem 564786 · easy · Level 05 Advanced Algorithms & Graphs

How Often Is Each Case Asked For?

py-decorators · py-closures · py-counter · recursion · call counting

Before speeding up a slow recursive function it helps to know which cases it recomputes and how often. Write a decorator tally that can be put above any function with @tally. The decorated function returns exactly what the original returns, and it gains two attributes:

  • calls: the total number of calls so far (an int, starting at 0);
  • by_args: a Counter (or a dictionary) mapping the tuple of positional arguments of each call to how many times that call was made.

A call that raises an exception still counts, and the exception reaches the caller unchanged. Each decorated function has its own counts. The decorated function must keep the original's __name__ and __doc__.

The tests decorate their own functions inside setup helpers that you can use with Run: fib_tally(n) (a recursive Fibonacci), paths_tally(rows, cols) (grid paths by plain recursion), keeps_metadata(), failing_tally(pairs) (a division that sometimes divides by zero), separate_counts() and cached_tally(n) (with functools.cache stacked on top). Each returns the counts it read from your attributes.

Examples

Input:  fib_tally(5)
Output: (5, 15, [((0,), 3), ((1,), 5), ((2,), 3), ((3,), 2), ((4,), 1), ((5,), 1)])

Input:  keeps_metadata()
Output: ("area", "Area of a width x height rectangle.", 6)

Constraints

  • The tests call decorated functions with positional arguments only, and the arguments are hashable.
  • Nothing is timed: only calls are counted.

Goals

  • Write a decorator that wraps any function without changing its results
  • Attach counters to the wrapper as attributes the caller can read
  • Keep the wrapped function's name and docstring with `functools.wraps`
Starting Python…