Functools
The functools module provides tools for working with functions and callable objects. Key utilities include caching, partial functions, and function wrapping.
lru_cache - Function Caching
Cache function results to avoid redundant computation:
from functools import lru_cache
import time
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100)) # Fast due to caching
# Cache info
print(fibonacci.cache_info())
# CacheInfo(hits=..., misses=..., maxsize=128, currsize=...)
# Clear cache
fibonacci.cache_clear()
Tip: Use
maxsize=None for unlimited cache, or maxsize=1 for simple memoization.
cache (Python 3.9+)
Simpler than lru_cache - unlimited cache for immutable arguments:
from functools import cache
# Equivalent to @lru_cache(maxsize=None)
@cache
def factorial(n):
return n * factorial(n - 1) if n else 1
partial - Partial Application
Fix some arguments of a function:
from functools import partial
def power(base, exponent):
return base ** exponent
# Fix the exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5)) # 25
print(cube(5)) # 125
# Or fix the base
def greet(greeting, name):
return f"{greeting}, {name}!"
say_hello = partial(greet, "Hello")
print(say_hello("World")) # Hello, World!
singledispatch - Function Overloading
Dispatch functions based on argument type:
from functools import singledispatch
@singledispatch
def process(arg):
return f"Unknown: {arg}"
@process.register(int)
def _process_int(arg):
return f"Integer: {arg * 2}"
@process.register(str)
def _process_str(arg):
return f"String: {arg.upper()}"
@process.register(list)
def _process_list(arg):
return f"List with {len(arg)} items"
print(process(10)) # Integer: 20
print(process("hello")) # String: HELLO
print(process([1, 2])) # List with 2 items
wraps - Preserve Function Metadata
Always use @wraps when writing decorators:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("Before")
return func(*args, **kwargs)
return wrapper
@my_decorator
def example():
"""This is the docstring."""
pass
print(example.__name__)
print(example.__doc__)
print(example.__annotations__)
reduce
Apply function cumulatively:
from functools import reduce
import operator
# Sum all numbers
print(reduce(lambda a, b: a + b, [1, 2, 3, 4])) # 10
# Using operator module (faster)
print(reduce(operator.add, [1, 2, 3, 4])) # 10
# With initial value
print(reduce(operator.add, [1, 2, 3], 10)) # 16
# Flatten nested lists
nested = [[1, 2], [3, 4], [5]]
print(reduce(lambda a, b: a + b, nested)) # [1, 2, 3, 4, 5]
cmp_to_key
Convert comparison functions to key functions:
from functools import cmp_to_key
def case_insensitive_cmp(a, b):
a = a.lower()
b = b.lower()
if a < b: return -1
if a > b: return 1
return 0
names = ["apple", "Banana", "cherry", "DATE"]
names.sort(key=cmp_to_key(case_insensitive_cmp))
print(names) # ['apple', 'Banana', 'cherry', 'DATE']
total_ordering
Auto-generate comparison methods:
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major, minor, patch):
self.version = (major, minor, patch)
def __eq__(self, other):
return self.version == other.version
def __lt__(self, other):
return self.version < other.version
v1 = Version(1, 2, 0)
v2 = Version(1, 2, 1)
print(v1 < v2) # True
print(v1 <= v2) # True
print(v1 > v2) # False
print(v1 >= v2) # False
print(v1 == v2) # False
cached_property (Python 3.8+)
Cache property values:
from functools import cached_property
import time
class DataLoader:
def __init__(self, data_id):
self.data_id = data_id
@cached_property
def data(self):
print(f"Loading data {self.data_id}...")
time.sleep(1) # Simulate slow load
return [1, 2, 3]
loader = DataLoader(42)
print(loader.data) # Loading... then [1, 2, 3]
print(loader.data) # [1, 2, 3] - cached!