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!