|$ curl https://forge-ai.dev/api/markdown?path=docs/python/functools
$cat docs/functools.md
updated Today·18-24 min read·published
functools
Introduction
functools provides higher-order tools: memoization, partial application, reducing iterables, decorating with metadata preserved, and single-dispatch generics.
lru_cache & cache
lru_cache.py
Python
| 1 | from functools import lru_cache, cache |
| 2 | |
| 3 | @lru_cache(maxsize=128) |
| 4 | def fib(n: int) -> int: |
| 5 | if n < 2: |
| 6 | return n |
| 7 | return fib(n - 1) + fib(n - 2) |
| 8 | |
| 9 | print(fib(30), fib.cache_info()) |
| 10 | fib.cache_clear() |
| 11 | |
| 12 | @cache # 3.9+ unbounded; same as lru_cache(maxsize=None) |
| 13 | def load_config(path: str) -> dict: |
| 14 | return {"path": path} |
| 15 | |
| 16 | # Arguments must be hashable |
| 17 | # @cache on methods: self breaks caching unless frozen/slots carefully |
| 18 | # Prefer module-level functions or cached_property for instances |
| Decorator | When |
|---|---|
| @cache | Pure functions, unbounded OK |
| @lru_cache(maxsize=N) | Bound memory; tune N |
| @lru_cache(typed=True) | Distinguish 1 vs 1.0 |
⚠
warning
Do not cache functions with side effects or mutable/global-dependent results.
partial
partial.py
Python
| 1 | from functools import partial |
| 2 | import json |
| 3 | |
| 4 | dumps_pretty = partial(json.dumps, indent=2, sort_keys=True) |
| 5 | print(dumps_pretty({"b": 1, "a": 2})) |
| 6 | |
| 7 | def power(base: int, exp: int) -> int: |
| 8 | return base ** exp |
| 9 | |
| 10 | square = partial(power, exp=2) |
| 11 | cube = partial(power, exp=3) |
| 12 | print(square(5), cube(3)) |
| 13 | |
| 14 | # Great with map / callbacks |
| 15 | from pathlib import Path |
| 16 | read_utf8 = partial(Path.read_text, encoding="utf-8") |
| 17 | print(read_utf8(Path("README.md") if Path("README.md").exists() else Path("/etc/hosts"))[:40]) |
reduce
reduce.py
Python
| 1 | from functools import reduce |
| 2 | from operator import add, mul, or_ |
| 3 | |
| 4 | print(reduce(add, [1, 2, 3, 4], 0)) |
| 5 | print(reduce(mul, [1, 2, 3, 4], 1)) |
| 6 | print(reduce(or_, [0b001, 0b010, 0b100])) |
| 7 | |
| 8 | # Prefer sum/any/all/str.join when they fit — clearer than reduce |
| 9 | # reduce shines for tree folds and custom monoids |
| 10 | def merge(a: dict, b: dict) -> dict: |
| 11 | return {**a, **b} |
| 12 | |
| 13 | print(reduce(merge, [{"a": 1}, {"b": 2}, {"a": 3}], {})) |
wraps
Always use @wraps(fn) in decorators so __name__, __doc__, and introspection survive.
wraps.py
Python
| 1 | from functools import wraps |
| 2 | import time |
| 3 | from collections.abc import Callable |
| 4 | from typing import TypeVar |
| 5 | |
| 6 | R = TypeVar("R") |
| 7 | |
| 8 | def timed(fn: Callable[..., R]) -> Callable[..., R]: |
| 9 | @wraps(fn) |
| 10 | def wrapper(*args, **kwargs) -> R: |
| 11 | t0 = time.perf_counter() |
| 12 | try: |
| 13 | return fn(*args, **kwargs) |
| 14 | finally: |
| 15 | print(f"{fn.__name__}: {time.perf_counter() - t0:.4f}s") |
| 16 | return wrapper |
| 17 | |
| 18 | @timed |
| 19 | def work(n: int) -> int: |
| 20 | """Sum 0..n-1.""" |
| 21 | return sum(range(n)) |
| 22 | |
| 23 | print(work.__name__, work.__doc__) |
| 24 | print(work(1_000_00)) |
singledispatch
singledispatch.py
Python
| 1 | from functools import singledispatch, singledispatchmethod |
| 2 | from datetime import date |
| 3 | |
| 4 | @singledispatch |
| 5 | def serialize(obj) -> str: |
| 6 | return repr(obj) |
| 7 | |
| 8 | @serialize.register |
| 9 | def _(obj: list) -> str: |
| 10 | return "[" + ", ".join(serialize(x) for x in obj) + "]" |
| 11 | |
| 12 | @serialize.register |
| 13 | def _(obj: date) -> str: |
| 14 | return obj.isoformat() |
| 15 | |
| 16 | @serialize.register(int) |
| 17 | def _(obj: int) -> str: |
| 18 | return f"int:{obj}" |
| 19 | |
| 20 | print(serialize([1, date.today()])) |
| 21 | |
| 22 | class Negator: |
| 23 | @singledispatchmethod |
| 24 | def neg(self, arg): |
| 25 | raise NotImplementedError(type(arg)) |
| 26 | |
| 27 | @neg.register |
| 28 | def _(self, arg: int) -> int: |
| 29 | return -arg |
| 30 | |
| 31 | @neg.register |
| 32 | def _(self, arg: bool) -> bool: |
| 33 | return not arg |
| 34 | |
| 35 | print(Negator().neg(5), Negator().neg(True)) |
cached_property
cached_property.py
Python
| 1 | from functools import cached_property |
| 2 | |
| 3 | class Report: |
| 4 | def __init__(self, path: str) -> None: |
| 5 | self.path = path |
| 6 | |
| 7 | @cached_property |
| 8 | def text(self) -> str: |
| 9 | print("loading") |
| 10 | return open(self.path, encoding="utf-8").read() if False else "body" |
| 11 | |
| 12 | @cached_property |
| 13 | def lines(self) -> list[str]: |
| 14 | return self.text.splitlines() |
| 15 | |
| 16 | r = Report("x") |
| 17 | print(r.lines, r.lines) # loading once |
| 18 | |
| 19 | # Clear cache: |
| 20 | # del r.text |
📝
note
cached_property is not thread-safe by default for the first compute — fine for typical single-threaded use.
total_ordering & Others
misc.py
Python
| 1 | from functools import total_ordering, cmp_to_key |
| 2 | |
| 3 | @total_ordering |
| 4 | class Version: |
| 5 | def __init__(self, s: str) -> None: |
| 6 | self.parts = tuple(int(x) for x in s.split(".")) |
| 7 | def __eq__(self, other: object) -> bool: |
| 8 | if not isinstance(other, Version): |
| 9 | return NotImplemented |
| 10 | return self.parts == other.parts |
| 11 | def __lt__(self, other: object) -> bool: |
| 12 | if not isinstance(other, Version): |
| 13 | return NotImplemented |
| 14 | return self.parts < other.parts |
| 15 | |
| 16 | print(sorted([Version("1.10"), Version("1.2"), Version("1.9")])) |
| 17 | |
| 18 | # cmp_to_key adapts old cmp functions to key= |
| 19 | def cmp(a, b): |
| 20 | return (a > b) - (a < b) |
| 21 | |
| 22 | print(sorted([3, 1, 2], key=cmp_to_key(cmp))) |
Production Patterns
- Cache at clear boundaries (parsed config, expensive pure transforms)
- Expose cache_clear in tests and on config reload
- partial for binding dependency-injected helpers
- wraps on every decorator you ship
- singledispatch for open extension without giant if/elif
pattern.py
Python
| 1 | from functools import lru_cache, wraps |
| 2 | from collections.abc import Callable |
| 3 | |
| 4 | def memoize_method(fn): |
| 5 | """Per-instance LRU via hidden cache attribute.""" |
| 6 | attr = f"_cache_{fn.__name__}" |
| 7 | @wraps(fn) |
| 8 | def wrapper(self, *args): |
| 9 | cache = getattr(self, attr, None) |
| 10 | if cache is None: |
| 11 | cache = lru_cache(maxsize=64)(lambda *a: fn(self, *a)) |
| 12 | setattr(self, attr, cache) |
| 13 | return cache(*args) |
| 14 | return wrapper |
update_wrapper & WRAPPER_ASSIGNMENTS
update_wrapper.py
Python
| 1 | from functools import update_wrapper, WRAPPER_ASSIGNMENTS, WRAPPER_UPDATES |
| 2 | |
| 3 | def decorator(fn): |
| 4 | def wrapper(*args, **kwargs): |
| 5 | return fn(*args, **kwargs) |
| 6 | update_wrapper(wrapper, fn) |
| 7 | # wraps is sugar for this |
| 8 | print(WRAPPER_ASSIGNMENTS) # __module__, __name__, __qualname__, __doc__, __annotations__ |
| 9 | print(WRAPPER_UPDATES) # __dict__ |
| 10 | return wrapper |
partialmethod
partialmethod.py
Python
| 1 | from functools import partialmethod |
| 2 | |
| 3 | class Cell: |
| 4 | def __init__(self): |
| 5 | self._alive = False |
| 6 | def set_state(self, state: bool) -> None: |
| 7 | self._alive = state |
| 8 | set_alive = partialmethod(set_state, True) |
| 9 | set_dead = partialmethod(set_state, False) |
| 10 | |
| 11 | c = Cell() |
| 12 | c.set_alive() |
| 13 | assert c._alive is True |
| 14 | c.set_dead() |
| 15 | assert c._alive is False |
Decision Guide
| Goal | Tool |
|---|---|
| Memoize pure fn | @cache / @lru_cache |
| Memoize attribute | cached_property |
| Bind args | partial / partialmethod |
| Preserve decorator meta | wraps |
| Type-based overloads at runtime | singledispatch |
| Fold sequence | reduce (or sum/join) |
| Fill comparison methods | total_ordering |
$Blueprint — Engineering Documentation·Section ID: PYTHON-FUNCTOOLS·Revision: 1.0
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.