We can use decorators to function memoization, so as not to run the function next time with the same argument, but instead take the result from cache. To do that, we define the `cache` variable (dictionary, in fact) above the local scope of the wrapper. This way, this dictionary remains available even after the call of the function finishes. It 'captures  This is an example of the 'closure' concept in program design. So, in the next call, we could check the cache. Note, how the function argument here is used as a dictionary's key. As such, the argument should be of a hashable type.

1
2
3
4
5
6
7
def cache_result(func):
    cache = {}
    def wrapper_memo(arg):
        res = cache[arg] if arg in cache else func(arg)
        cache[arg] = res
        return res
    return wrapper_memo

The classic example of function memoization is the Fibonacci Sequence calculation. In order to not calculate fib(1), fib(2), etc., over and over, we just extend `cache` dictionary, and take the result at another level of recursion, if it was calculated earlier.

 8
 9
10
@cache_result
def fib(n):
    return 1 if n <= 2 else fib(n-1) + fib(n-2)

Try this with and without @cache_result decorator, and feel the difference. Without memorizing the result from earlier recursion levels, you'll hardly see the result with n=40, for example...

11
print(fib(35))