Course outline · 0% complete

0/27 lessons0%

Course overview →

functools and math

lesson 6-3 · ~11 min · 17/27

lru_cache: the decorator you already understand

In lesson 5-2 you sketched a caching decorator. The standard library ships a production-grade one, functools.lru_cache. Stick it on any pure function (same inputs, same output, no side effects) and repeat calls become dictionary lookups:

from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

Without the cache, fib(35) recomputes the same subproblems millions of times. With it, each fib(k) is computed once. This technique, memoization, reappears as dynamic programming in the Algorithms course.

Caching is the rare optimization that is both free to add and safe, provided the function really is pure, which is why lru_cache shows up in virtually every serious Python codebase.

Caching turns exponential into instant

Naive recursive Fibonacci recomputes the same subproblems an astronomical number of times. With lru_cache every subresult is stored the first time it is computed, so fib(80) returns immediately.

from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(10))
print(fib(80))
print(fib.cache_info().hits > 0)

Output

55
23416728348467685
True

Without the decorator, fib(80) would take longer than you are willing to wait, since the call tree grows exponentially. The cache collapses it to about 80 real computations. fib.cache_info() is a bonus the decorator attaches to the function, in exactly the attach-state style you saw in lesson 5-2, and its hits field counts how many calls were answered from the cache.

The math module in sixty seconds

You used + - * / // % ** in Python for Beginners. math adds the rest:

import math

math.sqrt(2)       # 1.4142135623730951
math.floor(3.7)    # 3, round down
math.ceil(3.2)     # 4, round up
math.gcd(12, 18)   # 6, greatest common divisor
math.pi            # 3.141592653589793
math.inf           # infinity, a useful "worse than anything" start value

math.inf deserves a note: when hunting for a minimum, initialize best = math.inf and any real value will beat it. You will use exactly that trick in the unit 10 capstone.

Finding a minimum with an infinite starting point

The math.inf pattern gives a search a starting value that every real candidate beats. best begins at infinity, and each price replaces it if it is smaller.

import math

prices = [19.99, 12.50, 24.00, 15.75]

best = math.inf
for price in prices:
    best = min(best, price)
print(best)

Output

12.5

The two lines that matter are best = math.inf before the loop and best = min(best, price) inside it. Together they handle an empty list gracefully too, leaving best as infinity rather than crashing. Python prints 12.50 as 12.5 because trailing zeros are not part of the float's value, so use string formatting when you need to display money with fixed decimals.

When caching is safe

lru_cache belongs on pure functions: functions whose result depends only on their arguments, and which have no side effects.

The reason is mechanical. On a repeat call the cache hands back a stored result and never runs the body at all. If the body reads changing state, such as the current time, a file, or a global counter, the cached answer goes stale. If the body has side effects like printing or writing to a database, skipping it silently changes what your program does.

FunctionCacheableWhy
fib(n)yessame n always gives the same answer
parse(text)yesdepends only on the input string
read_config()nothe file on disk can change
log(msg)nothe printing is the point

One more requirement: the arguments have to be hashable, so a cached function can take a tuple but not a list.

Why the sentinel must lose to everything

When hunting for a minimum, best = math.inf works and best = 0 does not.

The starting value has to lose to every genuine candidate, otherwise it can survive to the end and be reported as an answer that was never in the data. With best = 0 and prices like [19.99, 12.50], min(best, price) keeps 0 on every iteration, and the program confidently prints a price that does not exist.

math.inf compares as larger than every real number, so the first actual value always replaces it. When hunting for a maximum, start at -math.inf for the mirror-image reason.