3.3. Functools#

functools is a built-in Python library to work with functions efficiently. This section will show you some useful methods of functools.

3.3.1. Simplifying Repetitive Function Calls with partial in Python#

Repeatedly calling functions with some fixed arguments results in redundant code and reduced readability, causing unnecessary repetition throughout your codebase.

# Without partial, you repeat the binning parameters for each column
import pandas as pd

df = pd.DataFrame({
    'salary': [45000, 75000, 125000, 85000],
    'bonus': [5000, 15000, 25000, 10000],
    'revenue': [150000, 280000, 420000, 310000]
})

processed_df = df.copy()

# Repetitive binning operations
processed_df['salary_level'] = pd.qcut(processed_df['salary'], q=4, labels=['Q1', 'Q2', 'Q3', 'Q4'])
processed_df['bonus_level'] = pd.qcut(processed_df['bonus'], q=4, labels=['Q1', 'Q2', 'Q3', 'Q4'])
processed_df['revenue_level'] = pd.qcut(processed_df['revenue'], q=4, labels=['Q1', 'Q2', 'Q3', 'Q4'])
processed_df
salary bonus revenue salary_level bonus_level revenue_level
0 45000 5000 150000 Q1 Q1 Q1
1 75000 15000 280000 Q2 Q3 Q2
2 125000 25000 420000 Q4 Q4 Q4
3 85000 10000 310000 Q3 Q2 Q3

With functools.partial, you can create new function variations with pre-set arguments, making your code more concise and maintainable.

from functools import partial

processed_df = df.copy()

# Create a standardized quartile binning function
quartile_bin = partial(pd.qcut, q=4, labels=["Q1", "Q2", "Q3", "Q4"])

# Apply the binning function consistently
processed_df["salary_level"] = quartile_bin(processed_df["salary"])
processed_df["bonus_level"] = quartile_bin(processed_df["bonus"])
processed_df["revenue_level"] = quartile_bin(processed_df["revenue"])
processed_df
salary bonus revenue salary_level bonus_level revenue_level
0 45000 5000 150000 Q1 Q1 Q1
1 75000 15000 280000 Q2 Q3 Q2
2 125000 25000 420000 Q4 Q4 Q4
3 85000 10000 310000 Q3 Q2 Q3

In this example, partial creates a standardized binning function with pre-set parameters for the number of quantiles and their labels. This ensures consistent binning across different columns.

If you need to change the binning strategy, you only need to modify it in one place:

processed_df = df.copy()

# Easy to create different binning strategies
quintile_bin = partial(pd.qcut, q=5, labels=["Bottom", "Low", "Mid", "High", "Top"])

processed_df["salary_level"] = quintile_bin(processed_df["salary"])
processed_df["bonus_level"] = quintile_bin(processed_df["bonus"])
processed_df["revenue_level"] = quintile_bin(processed_df["revenue"])
processed_df
salary bonus revenue salary_level bonus_level revenue_level
0 45000 5000 150000 Bottom Bottom Bottom
1 75000 15000 280000 Low High Low
2 125000 25000 420000 Top Top Top
3 85000 10000 310000 High Low High

3.3.2. functools.singledispatch: Call Another Function Based on the Type of the Current Function’s Argument#

Normally, to call another function based on the type of the current function’s argument, we use an if-else statement:

data = {"a": [1, 2, 3], "b": [4, 5, 6]}
data2 = [{"a": [1, 2, 3]}, {"b": [4, 5, 6]}]
def process_data(data):
    if isinstance(data, dict):
        process_dict(data)

    else:
        process_list(data)


def process_dict(data: dict):
    print("Dict is processed")


def process_list(data: list):
    print("List is processed")
process_data(data)
Dict is processed
process_data(data2)
List is processed

With singledispatch, you don’t need to use an if-else statement to call an appropriate function. singledispatch will choose the right function based on the type of current function’s first argument.

from functools import singledispatch


@singledispatch
def process_data2(data):
    raise NotImplementedError("Please implement process_data2")


@process_data2.register
def process_dict2(data: dict):
    print("Dict is processed")


@process_data2.register
def process_list2(data: list):
    print("List is processed")
process_data2(data)
Dict is processed
process_data2(data2)
List is processed

3.3.3. functools.reduce: Apply Function Cumulatively to the Items of Iterable#

If you want to apply a function of two arguments cumulatively to the items of iterable from left to right, use functools’s reduce. This method reduces the iterable to a single value.

In the code below, 3 is the result of the function add_nums(2, 1). 3 is then used as the first argument of the function add_nums(3, 2).

from functools import reduce


def add_nums(num1, num2):
    res = num1 + num2
    print(f"{num1} + {num2} = {res}")
    return res


print(reduce(add_nums, [1, 2, 3], 2))
2 + 1 = 3
3 + 2 = 5
5 + 3 = 8
8

3.3.4. Combine Reduce and Operator Methods#

You can combine functools.reduce with a method from operator to achieve the similar functionality and make the code more readable.

import functools
import operator

# 2+1=3, 3+2=5, 5+3=8
functools.reduce(operator.add, [1, 2, 3], 2)
8