functools is the standard library's toolbox for working with functions: caching, pre-filling arguments, building decorators, generic dispatch and ordering.
At a glance
| Tool | What it does |
|---|---|
partial(f, *a, **kw) | New callable with some arguments pre-filled |
partialmethod | partial for methods defined in a class body |
reduce(f, it, init) | Fold an iterable into a single value |
cache / lru_cache(maxsize) | Memoize results by arguments |
cached_property | Compute an attribute once per instance, then store it |
wraps / update_wrapper | Copy name, docstring, etc. onto a wrapper |
total_ordering | Fill in <=, >, >= from __eq__ + one comparison |
singledispatch | Choose an implementation by the type of the first argument |
cmp_to_key | Turn an old-style comparator into a key= function |
partial & partialmethod
from functools import partial, partialmethod
def power(base, exp):
return base ** exp
square = partial(power, exp=2)
cube = partial(power, exp=3)
print(square(7), cube(2)) # 49 8
print(square.func.__name__, square.keywords) # power {'exp': 2}
parse_hex = partial(int, base=16)
print(list(map(parse_hex, ["ff", "10"]))) # [255, 16]
class Light:
def set(self, state):
self.state = state
return state
on = partialmethod(set, "on")
off = partialmethod(set, "off")
print(Light().on()) # onreduce
from functools import reduce
import operator
print(reduce(operator.add, [1, 2, 3, 4])) # 10
print(reduce(operator.mul, range(1, 6), 1)) # 120
print(reduce(lambda acc, d: {**acc, **d}, [{"a": 1}, {"b": 2}, {"a": 3}], {})) # {'a': 3, 'b': 2}
print(reduce(max, [3, 9, 2])) # 9cache & lru_cache
from functools import lru_cache, cache
@lru_cache(maxsize=2)
def square(x):
return x * x
square(1); square(2); square(1) # hit for 1
square(3) # evicts 2 (least recently used)
square(2) # miss again
info = square.cache_info()
print(info.hits, info.misses, info.currsize) # 1 4 2
square.cache_clear()
print(square.cache_info().currsize) # 0
@lru_cache(typed=True)
def kind(x):
return type(x).__name__
print(kind(1), kind(1.0)) # int float ← typed: 1 and 1.0 cached separately
@cache
def total(xs):
return sum(xs)
try:
total([1, 2]) # arguments are dict keys → must be hashable
except TypeError as e:
print(e) # unhashable type: 'list'
print(total((1, 2))) # 3@cache | @lru_cache(maxsize=128) | |
|---|---|---|
| Size | Unbounded | Bounded; evicts least-recently-used |
| Speed | Slightly faster (no bookkeeping) | Small LRU overhead |
| Use for | Recursion / DP with a bounded input space | Long-running services with unbounded inputs |
cached_property
from functools import cached_property
class Report:
def __init__(self, rows):
self.rows = rows
self.computed = 0
@cached_property
def total(self):
self.computed += 1 # expensive work, done once
return sum(self.rows)
r = Report([5, 10, 20])
print(r.total, r.total, r.computed) # 35 35 1
del r.total # invalidate: the next access recomputes
print(r.total, r.computed) # 35 2wraps & update_wrapper
from functools import wraps, update_wrapper
def plain(f):
def wrapper(*a, **kw):
return f(*a, **kw)
return wrapper
def proper(f):
@wraps(f) # = update_wrapper(wrapper, f)
def wrapper(*a, **kw):
return f(*a, **kw)
return wrapper
@plain
def area(r):
"""Circle area."""
@proper
def volume(r):
"""Sphere volume."""
print(area.__name__, area.__doc__) # wrapper None
print(volume.__name__, volume.__doc__) # volume Sphere volume.
print(volume.__wrapped__.__name__) # volume ← the original, for introspectiontotal_ordering
from functools import total_ordering
@total_ordering
class Grade:
order = "FDCBA"
def __init__(self, letter):
self.letter = letter
def __eq__(self, other):
return self.letter == other.letter
def __lt__(self, other):
return self.order.index(self.letter) < self.order.index(other.letter)
def __repr__(self):
return self.letter
print(Grade("B") > Grade("C"), Grade("A") <= Grade("A")) # True True
print(sorted([Grade("C"), Grade("A"), Grade("F")])) # [F, C, A]singledispatch — overloading by type
from functools import singledispatch, singledispatchmethod
@singledispatch
def to_json(value):
raise TypeError(f"can't serialise {type(value).__name__}")
@to_json.register
def _(value: int):
return str(value)
@to_json.register
def _(value: str):
return f'"{value}"'
@to_json.register(list)
def _(value):
return "[" + ", ".join(to_json(v) for v in value) + "]"
print(to_json([1, "a", [2]])) # [1, "a", [2]]
print(to_json(True)) # True ← bool is a subclass of int
try:
to_json(1.5)
except TypeError as e:
print(e) # can't serialise float
class Area:
@singledispatchmethod
def of(self, shape):
raise NotImplementedError
@of.register
def _(self, side: int):
return side * side
@of.register
def _(self, dims: tuple):
return dims[0] * dims[1]
print(Area().of(3), Area().of((2, 5))) # 9 10cmp_to_key & the operator module
from functools import cmp_to_key
from operator import itemgetter, attrgetter, methodcaller
def by_length_then_reverse_alpha(a, b):
if len(a) != len(b):
return len(a) - len(b)
return (a < b) - (a > b) # reverse alphabetical
words = ["bb", "a", "cc", "c"]
print(sorted(words, key=cmp_to_key(by_length_then_reverse_alpha))) # ['c', 'a', 'cc', 'bb']
print(itemgetter(0, 2)("xyz")) # ('x', 'z')
print(attrgetter("real")(3 + 4j)) # 3.0
print(list(map(methodcaller("upper"), ["a", "b"]))) # ['A', 'B']How does lru_cache work?
partial vs lambda?
How do you get function overloading in Python?