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
b. That is, it returns the value of
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))
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
clamp_01 is a function that does the same job as
clamp, but with parameter
a set to 0 and
b 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
b, but not yet applied to the final parameter
x. This is sometimes described as binding the function to the values of
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
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:
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
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])
partial returns a function - in this case a function that behaves like
clamp except that
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
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.
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
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
csv function still allows you to override the separator if you wish.
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.