Functools: Enhancing Function Flexibility

Author

Andres Monge

Published

December 17, 2024

Python’s functools module provides a collection of higher-order functions that operate on or return other functions.

Let’s explore four powerful tools from this module: partial, singledispatch, wraps, and partialmethod.

partial: Creating Specialized Functions

The partial function allows you to create new functions with pre-set arguments, effectively specializing existing functions for specific use cases

How it works

Code
from functools import partial

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

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

Example: Scaling data

Code
from functools import partial

def scale_data(value, factor):
    return value * factor

scale_by_2 = partial(scale_data, factor=2)
scale_by_10 = partial(scale_data, factor=10)

data = [1, 2, 3, 4, 5]
scaled_by_2 = list(map(scale_by_2, data))
scaled_by_10 = list(map(scale_by_10, data))

print(scaled_by_2)   # Output: [2, 4, 6, 8, 10]
print(scaled_by_10)  # Output: [10, 20, 30, 40, 50]
[2, 4, 6, 8, 10]
[10, 20, 30, 40, 50]

singledispatch: Function Overloading Made Easy

The singledispatch decorator enables function overloading based on the type of the first argument, allowing different implementations for different data types.

How it works

Code
from functools import singledispatch
from dataclasses import dataclass

@dataclass
class Book:
    title: str
    author: str
    year: int

    def __str__(self):
        return f"{self.title} by {self.author} ({self.year})"


@singledispatch
def process_data(data):
    return f"processing for {data}"

@process_data.register(int)
def _(data):
    return f"integer: {data}"

@process_data.register(str)
def _(data):
    return f"string: {data}"

@process_data.register(Book)
def _(data):
    return f"book: {data.title} written by {data.author} in {data.year}"


print(process_data(42))
print(process_data("Hello, World!"))
print(process_data(Book("1984", "George Orwell", 1949)))
integer: 42
string: Hello, World!
book: 1984 written by George Orwell in 1949

wraps: Preserving Function Metadata

The wraps decorator helps maintain the original function’s metadata when creating decorators, ensuring that important information is not lost

How it works

Code
import logging
from functools import wraps

# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def my_decorator(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        logger.info(f"Calling {f.__name__}")
        logger.info(f"Function docstring: {f.__doc__}")
        logger.info(f"Function annotations: {f.__annotations__}")
        print("Before function call")
        result = f(*args, **kwargs)
        print("After function call")
        return result
    return wrapper

@my_decorator
def greet(name: str) -> str:
    """Returns a friendly greeting"""
    return f"Hello, {name}!"

print(greet("Alice"))
print(f"Function name: {greet.__name__}")
print(f"Function docstring: {greet.__doc__}")
print(f"Function annotations: {greet.__annotations__}")
INFO:__main__:Calling greet
INFO:__main__:Function docstring: Returns a friendly greeting
INFO:__main__:Function annotations: {'name': <class 'str'>, 'return': <class 'str'>}
Before function call
After function call
Hello, Alice!
Function name: greet
Function docstring: Returns a friendly greeting
Function annotations: {'name': <class 'str'>, 'return': <class 'str'>}

partialmethod: Partial Application for Class Methods

Similar to partial, partialmethod allows partial application of arguments to methods within a class

How it works

Code
from functools import partialmethod

class Calculator:
    def add(self, x, y):
        return x + y

    add_five = partialmethod(add, 5)

calc = Calculator()
print(calc.add_five(10))  # Output: 15
15