Python functools Module

The functools module in Python provides higher-order functions that operate on or return other functions. These utilities include tools for creating, using, and modifying functions and callable objects.

Table of Contents

  1. Introduction
  2. Key Functions and Classes
    • cache
    • cached_property
    • cmp_to_key
    • lru_cache
    • partial
    • partialmethod
    • reduce
    • singledispatch
    • singledispatchmethod
    • update_wrapper
    • wraps
  3. Examples
    • Using partial for Function Customization
    • Implementing Caching with lru_cache
    • Single Dispatch Generic Functions
  4. Real-World Use Case
  5. Conclusion
  6. References

Introduction

The functools module provides a collection of functions for higher-order operations. These functions help in creating and working with functions and callable objects, allowing for more modular and reusable code.

Key Functions and Classes

cache

A decorator to cache a function’s return value each time it is called with the same arguments.

from functools import cache

@cache
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(10))  # 55

Output:

55

cached_property

Transforms a method of a class into a property whose value is computed once and then cached as a normal attribute for the life of the instance.

from functools import cached_property

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @cached_property
    def area(self):
        print("Calculating area")
        return 3.14159 * self.radius ** 2

c = Circle(5)
print(c.area)  # Calculating area 78.53975
print(c.area)  # 78.53975 (cached result)

Output:

Calculating area
78.53975
78.53975

cmp_to_key

Transforms an old-style comparison function to a key function.

from functools import cmp_to_key

def compare(a, b):
    return (a > b) - (a < b)

numbers = [5, 2, 9, 1, 5, 6]
sorted_numbers = sorted(numbers, key=cmp_to_key(compare))
print(sorted_numbers)  # [1, 2, 5, 5, 6, 9]

Output:

[1, 2, 5, 5, 6, 9]

lru_cache

A decorator to wrap a function with a Least Recently Used (LRU) cache.

from functools import lru_cache

@lru_cache(maxsize=32)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(10))  # 55

Output:

55

partial

Creates a new partial function which calls the original function with some arguments fixed.

from functools import partial

def multiply(x, y):
    return x * y

double = partial(multiply, 2)
print(double(5))  # 10

Output:

10

partialmethod

Creates a new partial method which calls the original method with some arguments fixed.

from functools import partialmethod

class MyClass:
    def greet(self, name, greeting="Hello"):
        return f"{greeting}, {name}!"

    greet_hello = partialmethod(greet, greeting="Hello")
    greet_hi = partialmethod(greet, greeting="Hi")

obj = MyClass()
print(obj.greet_hello("Alice"))  # Hello, Alice!
print(obj.greet_hi("Bob"))  # Hi, Bob!

Output:

Hello, Alice!
Hi, Bob!

reduce

Applies a function of two arguments cumulatively to the items of a sequence, reducing the sequence to a single value.

from functools import reduce

numbers = [1, 2, 3, 4, 5]
result = reduce(lambda x, y: x + y, numbers)
print(result)  # 15

Output:

15

singledispatch

Transforms a function into a single-dispatch generic function.

from functools import singledispatch

@singledispatch
def process(value):
    print(f"Default process: {value}")

@process.register(int)
def _(value):
    print(f"Processing integer: {value}")

@process.register(str)
def _(value):
    print(f"Processing string: {value}")

process(10)  # Processing integer: 10
process("hello")  # Processing string: hello
process([1, 2, 3])  # Default process: [1, 2, 3]

Output:

Processing integer: 10
Processing string: hello
Default process: [1, 2, 3]

singledispatchmethod

Transforms a method into a single-dispatch generic method.

from functools import singledispatchmethod

class Processor:
    @singledispatchmethod
    def process(self, value):
        print(f"Default process: {value}")

    @process.register
    def _(self, value: int):
        print(f"Processing integer: {value}")

    @process.register
    def _(self, value: str):
        print(f"Processing string: {value}")

proc = Processor()
proc.process(10)  # Processing integer: 10
proc.process("hello")  # Processing string: hello
proc.process([1, 2, 3])  # Default process: [1, 2, 3]

Output:

Processing integer: 10
Processing string: hello
Default process: [1, 2, 3]

update_wrapper

Updates a wrapper function to look more like the wrapped function by copying attributes.

from functools import update_wrapper

def my_decorator(f):
    def wrapper(*args, **kwargs):
        print("Something is happening before the function is called.")
        return f(*args, **kwargs)
    update_wrapper(wrapper, f)
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

say_hello()

Output:

Something is happening before the function is called.
Hello!

wraps

A decorator for updating a wrapper function to look more like the wrapped function.

from functools import wraps

def my_decorator(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        print("Something is happening before the function is called.")
        return f(*args, **kwargs)
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

say_hello()

Output:

Something is happening before the function is called.
Hello!

Examples

Using partial for Function Customization

from functools import partial

def power(base, exponent):
    return base ** exponent

square = partial(power, exponent=2)
cube = partial(power, exponent=3)

print(square(4))  # 16
print(cube(2))  # 8

Output:

16
8

Implementing Caching with lru_cache

from functools import lru_cache

@lru_cache(maxsize=None)
def factorial(n):
    if n == 0:
        return 1
    return n * factorial(n - 1)

print(factorial(5))  # 120
print(factorial.cache_info())  # CacheInfo(hits=0, misses=6, maxsize=None, currsize=6)

Output:

120
CacheInfo(hits=0, misses=6, maxsize=None, currsize=6)

Single Dispatch Generic Functions

from functools import singledispatch

@singledispatch
def fun(value):
    print(f"Default handler: {value}")

@fun.register(int)
def _(value):
    print(f"Handling integer: {value}")

@fun.register(str)
def _(value):
    print(f"Handling string: {value}")

fun(10)  # Handling integer: 10
fun("hello")  # Handling string: hello
fun([1, 2, 3])  # Default handler: [1, 2, 3]

Output:

Handling integer: 10
Handling string: hello
Default handler: [1, 2, 3]

Real-World Use Case

Memoization

Memoization is a common use case for lru_cache. It can be used to optimize recursive algorithms by storing the results of expensive function calls and reusing them when the same inputs occur again.

from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(50))  # 12586269025

Output:

12586269025

Conclusion

The functools module is for higher-order functions: functions that act on or return other functions. In general, any callable object can be treated as a function for the purposes of this module.

References

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top