Back to Posts
A variety of tools arranged on a black surface, including a screwdriver, drill, pliers, paintbrush, and various hardware. This image symbolizes the versatility and efficiency of the Python functools module for code optimization, showcasing how different tools can enhance coding projects.

Optimizing Code Efficiency with Python functools

By Alyce Osbourne

Python has a highly versatile and efficient tool known as functools which serves as a reliable solution to common coding obstacles. From tasks such as code optimization through memoization, function enhancement using decorators, and proficient management of different data inputs, the functools module has you covered. Let’s take a look at how it can enhance your projects.

Decorators with functools.wraps

Decorators are a powerful feature in Python, allowing you to modify or enhance functions and methods. The functools.wraps decorator is used to ensure that the decorated function retains its original attributes, like the name and docstring.

import functools
from typing import Callable

def decorator[**P, R](func: Callable[P, R]) -> Callable[P, R]:
    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print(f'Calling function {func.__name__}')
        return func(*args, **kwargs)
    return wrapper

Memorization with functools.lru_cache

Memoization is a technique to cache the results of expensive function calls and reuse them when the same inputs occur again. The functools.lru_cache decorator provides an easy way to add memoization to your functions.

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

Single dispatch generic functions with functools.singledispatchmethod

The functools.singledispatchmethod decorator allows you to create generic methods that can handle different types of input data. This is useful for implementing polymorphic behavior based on input types.

class Dispatched:
    @functools.singledispatchmethod
    def process(self, data):
        raise NotImplementedError('Unsupported data type')

    @process.register
    def _(self, data: int):
        print('Processing integer', data)

    @process.register
    def _(self, data: str):
        print('Processing string', data)

    @process.register
    def _(self, data: float):
        print('Processing float', data)

d = Dispatched()
d.process(10) # Processing integer 10
d.process('hello') # Processing string hello
d.process(10.5) # Processing float 10.5
d.process([1, 2, 3]) # NotImplementedError("Unsupported data type")

Partial Functions with functools.partial

Partial functions allow you to fix a certain number of arguments of a function and generate a new function. The functools.partial function is used to create these partial functions.

def add(a: int, b: int) -> int:
    return a + b

add_10 = functools.partial(add, 10)

print(add_10(10)) # 20
print(add_10(5)) # 15

Reducing a sequence with functools.reduce

The functools.reduce function applies a binary function cumulatively to the items of a sequence, from left to right, to reduce the sequence to a single value. This is similar to folding or accumulating a list in other programming languages.

def mult(a: int, b: int) -> int:
    return a * b

numbers = [1, 2, 3, 4, 5]
print(functools.reduce(mult, numbers))  # Output: 120

Automatically implement ordering

The functools.total_ordering decorator allows for more efficiently written code by simplifying the process of implementing comparison dunder methods. Only one method needs to be implemented, with the rest being automatically generated from this one method. The main caveat of this decorator is that it increases the overhead of these operations due to it calling the defined comparison methods internally multiple times, so there is a tradeoff between simplicity and speed.

@functools.total_ordering
class Number[T: (int, float)]:
    def __init__(self, value: T):
        self.value = value

    def __eq__(self, other: "Number") -> bool:
        return self.value == other.value

    def __lt__(self, other: "Number") -> bool:
        return self.value < other.value

Final thoughts

functools is one of my favorite modules for a reason. By providing a number of useful tools, from memoization to improving our decorators, it excels at making your functional code just a little more useful and clean.

Improve your code with my 3-part code diagnosis framework

Watch my free 30 minutes code diagnosis workshop on how to quickly detect problems in your code and review your code more effectively.

When you sign up, you'll get an email from me regularly with additional free content. You can unsubscribe at any time.

Recent posts