functools provides tools for working with functions. Here are the most useful ones.
lru_cache
Memoize function results:
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
fibonacci(100) # Instant, not 2^100 calls
# Check cache stats
fibonacci.cache_info()
# CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)
# Clear cache
fibonacci.cache_clear()For unbounded cache:
@lru_cache(maxsize=None)
def expensive_computation(x):
...cache (Python 3.9+)
Simpler unbounded cache:
from functools import cache
@cache
def factorial(n):
return n * factorial(n-1) if n else 1cached_property (Python 3.8+)
Cache property on first access:
from functools import cached_property
class DataAnalyzer:
def __init__(self, data):
self.data = data
@cached_property
def statistics(self):
# Expensive computation, done once
return {
"mean": sum(self.data) / len(self.data),
"max": max(self.data),
"min": min(self.data),
}partial
Pre-fill function arguments:
from functools import partial
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
square(5) # 25
cube(3) # 27
# Useful with callbacks
import json
pretty_json = partial(json.dumps, indent=2, sort_keys=True)
print(pretty_json({"b": 2, "a": 1}))partialmethod
Like partial, but for methods:
from functools import partialmethod
class Cell:
def __init__(self):
self.alive = False
def set_state(self, state):
self.alive = state
# Create convenience methods
set_alive = partialmethod(set_state, True)
set_dead = partialmethod(set_state, False)
cell = Cell()
cell.set_alive() # cell.alive = Truereduce
Reduce iterable to single value:
from functools import reduce
# Sum (use sum() instead in practice)
reduce(lambda x, y: x + y, [1, 2, 3, 4]) # 10
# Product
reduce(lambda x, y: x * y, [1, 2, 3, 4]) # 24
# With initial value
reduce(lambda x, y: x + y, [1, 2, 3], 10) # 16
# Flatten nested list
nested = [[1, 2], [3, 4], [5, 6]]
reduce(lambda x, y: x + y, nested) # [1, 2, 3, 4, 5, 6]
# Find max (use max() instead)
reduce(lambda x, y: x if x > y else y, [3, 1, 4, 1, 5]) # 5wraps
Preserve function metadata in decorators:
from functools import wraps
def my_decorator(func):
@wraps(func) # Copies __name__, __doc__, etc.
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name):
"""Greet someone."""
return f"Hello, {name}!"
greet.__name__ # "greet" (not "wrapper")
greet.__doc__ # "Greet someone."singledispatch
Overload functions by type:
from functools import singledispatch
@singledispatch
def process(data):
raise NotImplementedError(f"Cannot process {type(data)}")
@process.register
def _(data: str):
return f"String: {data.upper()}"
@process.register
def _(data: int):
return f"Integer: {data * 2}"
@process.register
def _(data: list):
return f"List with {len(data)} items"
process("hello") # "String: HELLO"
process(5) # "Integer: 10"
process([1, 2]) # "List with 2 items"For methods, use singledispatchmethod:
from functools import singledispatchmethod
class Processor:
@singledispatchmethod
def process(self, data):
raise NotImplementedError()
@process.register
def _(self, data: str):
return data.upper()total_ordering
Auto-generate comparison methods:
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major, minor):
self.major = major
self.minor = minor
def __eq__(self, other):
return (self.major, self.minor) == (other.major, other.minor)
def __lt__(self, other):
return (self.major, self.minor) < (other.major, other.minor)
# Now has __le__, __gt__, __ge__ automatically
v1 = Version(1, 0)
v2 = Version(2, 0)
v1 < v2 # True
v1 <= v2 # True (auto-generated)
v1 >= v2 # False (auto-generated)cmp_to_key
Convert old-style comparison function:
from functools import cmp_to_key
def compare_length(a, b):
return len(a) - len(b)
words = ["apple", "pie", "banana"]
sorted(words, key=cmp_to_key(compare_length))
# ['pie', 'apple', 'banana']Practical Examples
Memoized API calls
from functools import lru_cache
@lru_cache(maxsize=100)
def fetch_user(user_id):
# Expensive API call
response = requests.get(f"/api/users/{user_id}")
return response.json()Configuration with defaults
from functools import partial
def send_email(to, subject, body, from_addr="noreply@example.com"):
...
# Pre-configured sender
send_notification = partial(send_email, from_addr="alerts@example.com")Type-based serialization
from functools import singledispatch
import json
from datetime import datetime
@singledispatch
def serialize(obj):
return str(obj)
@serialize.register
def _(obj: datetime):
return obj.isoformat()
@serialize.register
def _(obj: dict):
return {k: serialize(v) for k, v in obj.items()}Quick Reference
| Function | Use Case |
|---|---|
lru_cache | Memoize function results |
cache | Unbounded memoization |
cached_property | Cache property on first access |
partial | Pre-fill function arguments |
reduce | Reduce iterable to value |
wraps | Preserve function metadata |
singledispatch | Overload by argument type |
total_ordering | Generate comparison methods |
functools makes functional patterns practical in Python. Use it to write cleaner, more efficient code.
React to this post: