Why do I need functools.partial?
According to Clean Code by Bob Martin, functions should "do one thing", but sometimes we try to pack so many arguments in a function to generalize it, that the "one thing" aspect gets muddled or lost.
functools.partial
helps by making functions from other functions but give default values for certain attributes. functools.partialmethod
is the same idea, but within a class
. Examples should make this idea more concrete.
Example 1
A simple example (taken from Raymond Hettinger) is to create a custom function for a certain base when using the pow
function.
The following are equivalent:
2**3
>>> 8
pow(2,3)
>>> 8
If for some reason I would be using pow(2, x)
a lot, it might make sense to make a new function:
from functools import partial
power_of_2 = partial(pow, 2)
power_of_2(3)
>>> 8
The way this works is partial
takes a function for the first argument then takes the values for the arguments you want to freeze, left to right. It also takes keyward arguments, but we'll discuss that in the next section whether that's a good idea.
Freezing other arguments
If you want to freeze other arguments rather than the left one first, you have a few options.
Using Keyword Arguments
Here's a toy function that gets an employee's job level based on their position and location.
def get_employee_job_level(position=None, location=None):
level = position[0]
if location == 'CA':
return ord(level)
else:
return ord(level)//2
get_employee_job_level('CEO', 'CA')
>>> 67
If we were only interested in employees from California, we could use partial
to create a new function, like so:
from functools import partial
get_CA_employee_job_level = partial(get_employee_job_level, location='CA')
get_CA_employee_job_level('CEO')
>>> 67
The above isn't recommended, however, as discussed in the aforementioned tweet by Raymond Hettinger.
For example, this won't work if we want to use keyword arguments to make a square
function:
from functools import partial
square = partial(pow, y=2)
square(3)
>>> TypeError: pow() takes no keyword arguments
Using a lambda instead of keyword arguments
If one needs to freeze other arguments in a function, it's recommended to use a lambda
:
square = lambda x: pow(x, 2)
square(3)
>>> 9
Most useful case for partial
The above examples for partial
are nice, in terms of writing more clean and verbose code, but it just appears as something that's "nice" and not 100% necessary.
I think the most important use case is using it as a key for other functions, since these keys only allow one-argument functions to be passed.
For example:
def sort_func(x, y):
if x == 'forward':
return ord(y)
else:
return -ord(y)
backward_sort = partial(sort_func, 'backward')
letters = ['A', 'B', 'C', 'D', 'E']
sorted(letters, key=backward_sort)
>>> ['E', 'D', 'C', 'B', 'A']
Using in a Pandas DataFrame
This is especially useful in the Pandas apply
function for columns. If you have a function with important business logic, but has more than one argument, you can cut it down using partial
and pass it to the apply
function.
Using partialmethod
partialmethod
works in a similar fashion to partial
, but is used within a class
.
Take the following class
for example, which implements similar functionality to the functions we've made above.
from functools import partialmethod
class Compute:
def __init__(self, x, y):
self.x = x
self.y = y
self.power = self.my_pow(self.x, self.y)
def my_pow(self, x, y):
return x ** y
def __repr__(self):
return '<Compute {x},{y}>'.format(x=self.x, y=self.y)
power_of_2 = partialmethod(my_pow, 2)
num=Compute(2,3)
num
>>> <Compute 2,3>
num.power
>>> 8
num.power_of_2(2)
>>> 4
Basically we want to add another method for the class
that's based on a previous method.
Why not just use partial in the class?
If you run the above code, with partial
instead of partialmethod
you'll get an error. The reason is since, as we mentioned above, partial
freezes arguments from left to right. The first argument of a method is self
which, you can imagine, will make for many problems if you try to freeze that with something other than the object instance.
Summary
The partial
and partialmethod
functions are easy ways to create new functions when you need to "freeze" an argument from an existing one. The there are various applications, with the most useful being, in my experience, when you need to pass a one-argument function as an argument to another function, like Pandas' apply
or a key
parameter.
Comments