Partial functions are used to create new functions from an existing function by pre-filling some of its arguments and thus creating a new version of the original function. This is particularly useful when dealing with functions that require a large number of arguments. Instead of having to define each argument for every call, we can create a specialized version of the function with some arguments already pre-filled. We achieve this primarily using  the partial class from the functools module.

Consider the following example:

def power(a, b):
    return a ** b

print(power(4, 2))

We can use the partial class to create more specialized versions of the power() function above, as shown below.

import functools

def power(a, b):
    return a ** b

square = functools.partial(power, b = 2)
cube = functools.partial(power, b = 3)

print(square(5))
print(cube(5))

As shown above the partial constructor takes in a function as the required argument, it then takes the arbitrary positional and keyword arguments to pre-fill. The syntax is as follows:

partial(func, *args, **kwargs)

The function returns a callable object which is a specialized application of the original function in which the specified arguments have been pre-filled.

Consider the following example:

import functools

#create a specialized version of the builtin sorted() function
abs_sorted = functools.partial(sorted, key = abs)

#use the partial function to sort integers by their absolute value
L = [2, 0, 5, -3, -2, 1, -4, ]

print(abs_sorted(L))

In the above example, we created a version of the builtin sorted() function in which the objects are sorted by their absolute value by default.

partial with other callable objects

In reality, the partial class can be used with any callable object not just freestanding functions. In the following example we use the class to create a specialized version of a class constructor.

import functools

class Person:
    def __init__(self, name, country, nationality):
        self.name = name
        self.country = country
        self.nationality = nationality
    
    def info(self):
        return f"""name: {self.name}
country: {self.country}
nationality: {self.nationality}"""

indian = functools.partial(Person, country = 'India', nationality = 'Indian')

p1 = indian('Rahul')

print(p1.info())

Acquiring original properties

By default, the partial objects do not inherit the __name__  and  __doc__ attributes from the original function which. The two attributes can be really useful especially for debugging purposes.

import functools

def power(a, b):
    '''returns a ^ b'''
    return a ** b

#get the docstring
print(power.__doc__, '\n')

square = functools.partial(power, b = 2)

#get the docstring of the partial functions
print(square.__doc__)

We can use the update_wrapper() utility function from the module, to assign the properties of the original objects to the partial objects.

update_wrapper(wrapper, wrapped, ...)

The function copies the necessary properties from the original callable to the partial object.

import functools

def power(a, b):
    '''returns a ^ b'''
    return a ** b

square = functools.partial(power, b = 2)

#assign original properties to the square function
functools.update_wrapper(square, power)

print(square.__name__)
print(square.__doc__)