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.