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 (anint, starting at0);by_args: aCounter(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`