5.1. Case Study: Memoize
5.1.1. Environment
Date: 2024-07-11
Python: 3.12.4
IPython: 8.26.0
System: macOS 14.5
Computer: MacBook M3 Max
CPU: 16 cores (12 performance and 4 efficiency) / 3nm
RAM: 128 GB RAM LPDDR5
5.1.2. Recap
Recap information about factorial (n!
):
5! = 5 * 4!
4! = 4 * 3!
3! = 3 * 2!
2! = 2 * 1!
1! = 1 * 0!
0! = 1
factorial(5) # = 120
return 5 * factorial(4) # 5 * 24 = 120
return 4 * factorial(3) # 4 * 6 = 24
return 3 * factorial(2) # 3 * 2 = 6
return 2 * factorial(1) # 2 * 1 = 2
return 1 * factorial(0) # 1 * 1 = 1
return 1 # 1
5.1.3. No Cache
>>> def factorial(n: int) -> int:
... return 1 if n == 0 else n * factorial(n-1)
>>>
... %%timeit -r 1000 -n 1000
... factorial(50)
... factorial(49)
... factorial(51)
4.85 μs ± 237 ns per loop (mean ± std. dev. of 1000 runs, 1,000 loops each)
5.1.4. Global Scope
>>> _cache = {}
>>>
>>> def cache(func):
... def wrapper(n):
... if n not in _cache:
... _cache[n] = func(n)
... return _cache[n]
... return wrapper
>>> @cache
... def factorial(n: int) -> int:
... return 1 if n == 0 else n * factorial(n-1)
>>>
... %%timeit -r 1000 -n 1000
... factorial(50)
... factorial(49)
... factorial(51)
114 ns ± 25.4 ns per loop (mean ± std. dev. of 1000 runs, 1,000 loops each)
5.1.5. Local Scope
>>> def cache(func):
... _cache = {}
... def wrapper(n):
... if n not in _cache:
... _cache[n] = func(n)
... return _cache[n]
... return wrapper
>>> @cache
... def factorial(n: int) -> int:
... return 1 if n == 0 else n * factorial(n-1)
>>>
... %%timeit -r 1000 -n 1000
... factorial(50)
... factorial(49)
... factorial(51)
113 ns ± 25.8 ns per loop (mean ± std. dev. of 1000 runs, 1,000 loops each)
5.1.6. Embedded Scope
>>> def cache(func):
... def wrapper(n):
... if n not in wrapper.cache:
... wrapper.cache[n] = func(n)
... return wrapper.cache[n]
... if not hasattr(wrapper, 'cache'):
... setattr(wrapper, 'cache', {})
... return wrapper
>>> @cache
... def factorial(n: int) -> int:
... return 1 if n == 0 else n * factorial(n-1)
>>>
... %%timeit -r 1000 -n 1000
... factorial(50)
... factorial(49)
... factorial(51)
187 ns ± 37.7 ns per loop (mean ± std. dev. of 1000 runs, 1,000 loops each)
5.1.7. Contains
>>> _cache = {}
>>>
>>> def factorial(n: int) -> int:
... if n not in _cache:
... _cache[n] = 1 if n == 0 else n * factorial(n-1)
... return _cache[n]
>>>
... %%timeit -r 1000 -n 1000
... factorial(50)
... factorial(49)
... factorial(51)
107 ns ± 27.7 ns per loop (mean ± std. dev. of 1000 runs, 1,000 loops each)
5.1.8. Get
>>> _cache = {}
>>>
>>> def factorial(n: int) -> int:
... result = _cache.get(n)
... if result:
... return result
... if n == 0:
... return 1
... else:
... result = _cache[n] = n * factorial(n-1)
... return result
>>>
... %%timeit -r 1000 -n 1000
... factorial(50)
... factorial(49)
... factorial(51)
107 ns ± 18.6 ns per loop (mean ± std. dev. of 1000 runs, 1,000 loops each)
5.1.9. Exceptions 1
>>> _cache = {}
>>>
>>> def factorial(n: int) -> int:
... if n == 0:
... return 1
... try:
... return _cache[n]
... except KeyError:
... _cache[n] = result = n * factorial(n-1)
... return result
>>>
... %%timeit -r 1000 -n 1000
... factorial(50)
... factorial(49)
... factorial(51)
105 ns ± 25.4 ns per loop (mean ± std. dev. of 1000 runs, 1,000 loops each)
5.1.10. Exceptions 2
>>> _cache = {}
>>>
>>> def factorial(n: int) -> int:
... try:
... return _cache[n]
... except KeyError:
... _cache[n] = 1 if n == 0 else n * factorial(n-1)
... return _cache[n]
>>>
... %%timeit -r 1000 -n 1000
... factorial(50)
... factorial(49)
... factorial(51)
80.4 ns ± 15.2 ns per loop (mean ± std. dev. of 1000 runs, 1,000 loops each)
5.1.11. Layer
>>> _cache = {}
>>>
>>> def factorial(n: int) -> int:
... def factorial(n: int) -> int:
... if n == 0:
... return 1
... return n * factorial(n-1)
... if not n in _cache:
... _cache[n] = factorial(n)
... return _cache[n]
>>>
... %%timeit -r 1000 -n 1000
... factorial(50)
... factorial(49)
... factorial(51)
229 ns ± 28.8 ns per loop (mean ± std. dev. of 1000 runs, 1,000 loops each)
5.1.12. Get from cache
>>> _cache = {}
>>>
>>> def factorial(n: int) -> int:
... if n == 0:
... return 1
... if n in _cache:
... return _cache[n]
... result = _cache[n] = n * factorial(n-1)
... return result
>>>
... %%timeit -r 1000 -n 1000
... factorial(50)
... factorial(49)
... factorial(51)
153 µs ± 9.64 µs per loop (mean ± std. dev. of 1000 runs, 10000 loops each)
5.1.13. Assignment Expression
>>> # %%timeit -r 1000 -n 10_000
>>> _cache = {}
>>>
>>> def factorial(n):
... if n == 0:
... return 1
... if (result := _cache.get(n)):
... return result
... result = n * factorial(n-1)
... _cache[n] = result
... return result
>>>
>>>
>>> factorial(50)
>>> factorial(40)
>>> factorial(45)
153 µs ± 9.64 µs per loop (mean ± std. dev. of 1000 runs, 10000 loops each)