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