~20 min · functools, cache, partial, reduce, lru_cache
Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
The function-shaped utilities
If itertools is for iterables, functools is for functions. Caching results, currying arguments, reducing sequences, preserving metadata across decorator wrappers. The four worth knowing first: cache/lru_cache, partial, reduce, and wraps (covered in decorators track).
cache and lru_cache — memoization for free
@functools.cache (3.9+) caches results based on the arguments — same args, return the cached value. @lru_cache(maxsize=128) is the same with a bounded LRU cache. Pure functions become dramatically faster for repeated calls. The classic demo: recursive Fibonacci goes from exponential to linear.
partial — pre-binding arguments
functools.partial(fn, *args, **kwargs) returns a new callable with some arguments pre-filled. Useful for adapting a function to an interface that expects fewer arguments — passing it as a callback, sorting key, or event handler with the configuration baked in.
reduce — fold left over an iterable
functools.reduce(fn, iterable) applies fn cumulatively from the left, reducing the iterable to a single value. reduce(operator.add, [1, 2, 3]) is ((1+2)+3). Most uses of reduce are now better expressed with sum, min, max, or a comprehension. Reach for it when you need a custom combining function.
War Story:cache and lru_cache work by hashing the arguments. If your function takes unhashable types (lists, dicts), the cache decorator raises. Solution: use a wrapper that converts to a hashable form, or use cachetools which has more options.
Code
cache and lru_cache — memoization·python
from functools import cache, lru_cache
import time
# Without cache — recursive Fibonacci is exponential
def fib_slow(n):
if n < 2:
return n
return fib_slow(n - 1) + fib_slow(n - 2)
# With cache — linear
@cache
def fib_fast(n):
if n < 2:
return n
return fib_fast(n - 1) + fib_fast(n - 2)
# fib_slow(35) takes ~3 seconds
# fib_fast(35) is instant
print(fib_fast(50)) # 12586269025 — works for huge n
# lru_cache with size limit
@lru_cache(maxsize=128)
def expensive(x):
print("computing", x)
return x * x
print(expensive(3)) # computing 3, returns 9
print(expensive(3)) # cached, no print
print(expensive.cache_info()) # CacheInfo(hits=1, misses=1, maxsize=128, currsize=1)
from functools import reduce
import operator
# Most reduces have built-in equivalents
print(sum([1, 2, 3, 4])) # 10
print(reduce(operator.add, [1, 2, 3, 4])) # 10 (same)
# Where reduce earns its keep — custom combiner
products = reduce(operator.mul, [1, 2, 3, 4], 1) # 24
print(products)
# Custom function — find longest string
words = ["hi", "hello", "hey", "howdy"]
longest = reduce(lambda a, b: a if len(a) >= len(b) else b, words)
print(longest) # 'howdy'
# But max() with key= is usually clearer
print(max(words, key=len)) # 'howdy'
cache + unhashable arguments — the gotcha·python
from functools import cache
@cache
def sum_list(items):
return sum(items)
try:
sum_list([1, 2, 3]) # list is unhashable
except TypeError as e:
print("caught:", e)
# Workaround — convert to a hashable type at the boundary
@cache
def sum_tuple(items_tuple):
return sum(items_tuple)
print(sum_tuple((1, 2, 3))) # works
# Or write a wrapper
def sum_list_v2(items):
return sum_tuple(tuple(items))
(a) Decorate a recursive function that computes binomial coefficients (C(n, k) = C(n-1, k-1) + C(n-1, k)) with @cache and confirm large inputs are fast. Print cache_info() to see hits/misses. (b) Use partial to create a log_warn function that's print pre-bound with [WARN] as the first argument. (c) Use reduce with a custom function to compute the product of a list — then compare to math.prod.
Progress
Progress is local-only — sign in to sync across devices.