Partial application

By Martin McBride, 2020-09-03
Tags: partial application closure inner function function composition higher order function map
Categories: functional programming


Functional programming, as we have seen, is a paradigm in which functions form the fundamental building blocks.

It should be no surprise that functional programming includes several techniques for deriving new functions from existing functions. This is analogous to object oriented programming, where classes are the building block, and we have various ways to derive new classes from existing ones (inheritance and composition, for example). In both cases, the aim is the same - to reuse existing code (following the DRY principle, don't repeat yourself).

Partial application is one such technique.

What is partial application?

Here is a function that returns the value of x, clamped to the range a to b. That is, it returns the value of x unless x is less than a (in which case a is returned), or x is greater than b (in which case b is returned):

def clamp(a, b, x):
    return min(b, max(a, x))

The partial method (in the standard functools module) can be used to create a new function, that behaves like clamp, but with some of its arguments already set to fixed values. For example:

from functools import partial

clamp_01 = partial(clamp, 0, 1)
print(clamp_01(2))  # Prints 1

Here, clamp_01 is a function that does the same job as clamp, but with parameter a set to 0 andb set to 1. This means that clamp_01 only accepts a single parameter, x, that gets clamped between 0 and 1.

We call this technique partial application, because you can think of it behaving as if the clamp function had been applied to a and b, but not yet applied to the final parameter x. This is sometimes described as binding the function to the values of a and b.

The important thing to notice about partial is that it is a higher order function - a function that operates on other functions. It accepts a function as its first argument, and returns a brand new function as a result. partial itself doesn't do any clamping, it just creates a new clamping function.

Example of using partial

The map function is often used in functional programming, to apply a function to a sequence of values. For example:

s = map(neg, [1, -2, 0 -3, 2])

This code applies the built-in neg function to the list of values, creating a sequence of numbers as a result. The neg function simply negates the value (1 becomes -1, -2 becomes 2, etc). So it will create an output sequence (-1, 2, 0, 3, -2). If you try this code remember that map creates a lazy sequence, so you will need to convert it to a list if you want to print it:

print(list(s))

Now the important thing about map is that the function you pass into it must accept exactly one parameter. For example neg accepts one parameter, neg(x) returns the negative of x.

If we wanted to use map to apply 0 to 1 clamping to a sequence of numbers, we couldn't use clamp(0, 1, x) because it takes 3 parameters (even though 2 of them are constant). That is where partial comes in. We can do this:

map(partial(clamp, 0, 1), [1, -2, 0 -3, 2])

Remember that partial returns a function - in this case a function that behaves like clamp except that a and b are fixed, so the partial function only takes a single parameter x. That is exactly what we need!

Why use partial?

Now you may be thinking that there are other ways of doing this. That is certainly true. For example, you could create a special clamp function like this:

def my_clamp(x):
    return clamp(0, 1, x)

map(my_clamp, [1, -2, 0 -3, 2])

Or, if you didn't want to go to the trouble of creating a named function that you only needed to use once, you could use a lambda:

map(lambda x: clamp(0, 1, x), [1, -2, 0 -3, 2])

There is nothing terribly wrong with either of these solutions - they work, and they aren't overly complicated, inefficient or messy.

But they are procedural. The definition of the my_clamp function doesn't just request a version of clamp that has preset a and b values. It includes code that specifies how to achieve that. The problem is that a line of procedural code could be doing anything. You have to read and understand the code to be sure it is really doing what it claims to be doing. Sure, it isn't that difficult with a one-line function, but still, every line of procedural code is an accident waiting to happen.

On the other hand, the partial call is declarative. You say exactly what you want, but not how to do it:

partial(clamp, 0, 1)

Here, you are very clearly stating that you want a version of clamp that has its first two parameters set to 0 and 1. The code couldn't possibly mean anything else, and assuming you trust the partial function (which is part of core Python so you probably can) there isn't really anything that could go wrong.

By analogy, if you were writing code to search for a values a Python list, you probably wouldn't write your own code to do it. You could, and it wouldn't be that difficult. But you wouldn't because it would be pointless, and random, and anyone maintaining your code would first wonder why you would do such a thing, and then probably check your code very carefully to check that there isn't some clever reason why you aren't just using the built in function.

The same is true of partial. If you are using a functional programming paradigm, and you need to do a standard thing to a function, such as obtaining a partial function, it is best to just use the standard functions to do it.

Other uses

Partial application can be applied multiple times, for example:

non_neg = partial(clamp, 0)
...
byte_value = partial(non_neg, 255)

In this case, the first call creates a function non_neg(b, x), which is a variant of clamp that clamps the minimum value to 0 but still allows you to choose an upper limit, b. That means that code using this function can definitely never produce negative output.

At some point later in your code, you can call partial again to create byte_value(x), a function that further limits the values to a maximum value of 255 (as well as the previous minimum of 0).

You can also apply optional parameters using partial:

csv = partial(print, sep=",")
csv(1, 2, 3) # 1,2,3
csv(1, 2, 3, sep = "|") 1|2|3

This example creates a new function csv, that behaves exactly like print except the separator is a comma by default. Notice that the csv function still allows you to override the separator if you wish.

Summary

Functional programming is a useful aspect of the Python language that you can mix and match with procedural code. If you decide to use FP, it is worth knowing some of the standard utility functions that Python provides.

See also

If you found this article useful, you might be interested in the book NumPy Recipes or other books by the same author.

Join the PythonInformer Newsletter

Sign up using this form to receive an email when new content is added:

Popular tags

2d arrays abstract data type alignment and angle animation arc array arrays bar chart bar style behavioural pattern bezier curve built-in function callable object chain circle classes clipping close closure cmyk colour combinations comparison operator comprehension context context manager conversion count creational pattern data science data types decorator design pattern device space dictionary drawing duck typing efficiency ellipse else encryption enumerate fill filter font font style for loop formula function function composition function plot functools game development generativepy tutorial generator geometry gif global variable gradient greyscale higher order function hsl html image image processing imagesurface immutable object in operator index inner function input installing iter iterable iterator itertools join l system lambda function latex len lerp line line plot line style linear gradient linspace list list comprehension logical operator lru_cache magic method mandelbrot mandelbrot set map marker style matplotlib monad mutability named parameter numeric python numpy object open operator optimisation optional parameter or pandas partial application path pattern permutations pie chart pil pillow polygon pong positional parameter print product programming paradigms programming techniques pure function python standard library radial gradient range recipes rectangle recursion reduce regular polygon repeat rgb rotation roundrect scaling scatter plot scipy sector segment sequence setup shape singleton slice slicing sound spirograph sprite square str stream string stroke structural pattern subpath symmetric encryption template tex text text metrics tinkerbell fractal transform translation transparency triangle truthy value tuple turtle unpacking user space vectorisation webserver website while loop zip zip_longest