Skip to main content
Python

The functools library in Python is a treasure trove

5 mins

A treasure trove illustrating the rich functionality found in functools

What is the functools library in Python? #

The functools library in Python is a module that provides higher-order functions and operations on callable objects.

Higher-order functions are functions that either take other functions as arguments or return a function as their result, and is associated with functional programming paradigms.

You may have already used some of the functions in the functools library such as the @wraps decorator, which is used to preserve the metadata of the original function when creating a decorator. Here are some of the most commonly used functions in the functools library:

lru_cache #

Speed up function calls by caching results of expensive function calls.

The lru_cache decorator is a simple way to add a cache to your application. The cache is keyed by the function arguments, so if you call the function with the same arguments again, it will return the cached result instead of recomputing it. This is useful for calls that always return the same result for the same arguments, such as mathematical functions.

Note that care must be taken when using lru_cache on functions that use database queries, as the cache will not be invalidated when the database changes. It is better to use an alternative caching mechanism that is aware of the database state.

A good example of using lru_cache is to cache the results of a Fibonacci function, which is a common example in functional programming.

from functools import lru_cache

@lru_cache()
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(40))  # Output: 102334155

This one line can greatly speed up the computation, as the Fibonacci function is called multiple times with the same arguments.

See these results of running the Fibonacci function with and without lru_cache:

without lru_cache:

$ time python fibonacci.py 
102334155

real    0m5.782s
user    0m5.778s
sys     0m0.004s

with lru_cache:

$ time python fibonacci.py 
102334155

real    0m0.018s
user    0m0.012s
sys     0m0.006s

partial #

Create a new function with some arguments already set.

The partial function is used to create a new function with some of the arguments already set.

This is useful for a number of use cases.

  • customizing third-party functions that take a lot of arguments, so you can create a new function with the arguments you need, simplifying the call.
  • reducing calls with the same arguments multiple times.
  • creating a new function with a specific configuration, such as a logger that logs to a file with a specific format.
from functools import partial

def log(message, prefix, level='INFO'):
    print(f"{level}: {prefix} - {message}")

log_audit = partial(log, prefix='AUDIT')
log_error = partial(log, prefix='SYSTEM', level='ERROR')

log_audit("User logged in") # INFO: [AUDIT] - User logged in
log_error("User not found") # ERROR: [SYSTEM] - User not found

Now, only the partial function needs to updated if the logging format changes.

reduce #

Reduce a sequence to a single value by applying a function cumulatively.

The reduce function is used to reduce a sequence to a single value by applying a function cumulatively. Potential use cases can be:

  • summing a list of numbers
  • concatenating a list of strings
  • finding the maximum or minimum value in a list

reduce takes two arguments: a function and a sequence. The function should take two arguments and return a single value.

It will apply the provided function to the first two elements of the sequence, then apply the function again to the result and the next element, and so on, until the sequence is exhausted.

from functools import reduce

def add(x, y):
    return x + y
def multiply(x, y):
    return x * y
def concatenate(x, y):
    return x + y
def find_max(x, y):
    return max(x, y)
def find_min(x, y):
    return min(x, y)

numbers = [1, 2, 3, 4, 5]

print(reduce(add, numbers))  # Output: 15
print(reduce(multiply, numbers))  # Output: 120
print(reduce(concatenate, ['Hello', ' ', 'World']))  # Output: Hello World
print(reduce(find_max, numbers))  # Output: 5
print(reduce(find_min, numbers))  # Output: 1

You can even use lambda functions with reduce to create more concise code. For example, to sum a list of numbers:

from functools import reduce
numbers = [1, 2, 3, 4, 5]
sum_numbers = reduce(lambda x, y: x + y, numbers)
print(sum_numbers)  # Output: 15

This saves you from having to write a loop to iterate over the sequence and apply the function to each element.

total_ordering #

Create a total ordering from a partial ordering.

When you have a class that implements only some of the rich comparison methods (like __lt__, __le__, __gt__, __ge__), you can use the total_ordering decorator to automatically fill in the missing methods.

This is possible because some functions are the opposites of others; for example, less than and greater than are opposites, so if you implement one, you can derive the other.

By using total_ordering, you can avoid having to implement all the rich comparison methods yourself.

You will need to implement at least one of the rich comparison methods (__lt__, __le__, __gt__, __ge__) and the equality method (__eq__).

from functools import total_ordering

@total_ordering
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __lt__(self, other):
        return (self.x, self.y) < (other.x, other.y)

    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)

    def __repr__(self):
        return f"Point({self.x}, {self.y})"        
points = [Point(1, 2), Point(3, 4), Point(2, 1)]

# Greater than comparison works even though we only implemented __lt__ and __eq__
if (points[0] > points[1]):
    print(f"{points[0]} is greater than {points[1]}")
else:
    print(f"{points[0]} is not greater than {points[1]}")

# Output: Point(1, 2) is not greater than Point(3, 4)