Chapter 4. Higher-Order Functions

In the last chapter we saw an iterator algebra that builds on the itertools module. In some ways, higher-order functions (often abbreviated as “HOFs”) provide similar building blocks to express complex concepts by combining simpler functions into new functions. In general, a higher-order function is simply a function that takes one or more functions as arguments and/or produces a function as a result. Many interesting abstractions are available here. They allow chaining and combining higher-order functions in a manner analogous to how we can combine functions in itertools to produce new iterables.

A few useful higher-order functions are contained in the functools module, and a few others are built-ins. It is common the think of map(), filter(), and functools.reduce() as the most basic building blocks of higher-order functions, and most functional programming languages use these functions as their primitives (occasionally under other names). Almost as basic as map/filter/reduce as a building block is currying. In Python, currying is spelled as partial(), and is contained in the functools module—this is a function that will take another function, along with zero or more arguments to pre-fill, and return a function of fewer arguments that operates as the input function would when those arguments are passed to it.

The built-in functions map() and filter() are equivalent to comprehensions—especially now that generator comprehensions are available—and most Python programmers find the comprehension versions more readable. For example, here are some (almost) equivalent pairs:

# Classic "FP-style"
transformed = map(tranformation, iterator)
# Comprehension
transformed = (transformation(x) for x in iterator)

# Classic "FP-style"
filtered = filter(predicate, iterator)
# Comprehension
filtered = (x for x in iterator if predicate(x))

The function functools.reduce() is very general, very powerful, and very subtle to use to its full power. It takes successive items of an iterable, and combines them in some way. The most common use case for reduce() is probably covered by the built-in sum(), which is a more compact spelling of:

from functools import reduce
total = reduce(operator.add, it, 0)
# total = sum(it)

It may or may not be obvious that map() and filter() are also a special cases of reduce(). That is:

>>> add5 = lambda n: n+5
>>> reduce(lambda l, x: l+[add5(x)], range(10), [])
[5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
>>> # simpler: map(add5, range(10))
>>> isOdd = lambda n: n%2
>>> reduce(lambda l, x: l+[x] if isOdd(x) else l, range(10), [])
[1, 3, 5, 7, 9]
>>> # simpler: filter(isOdd, range(10))

These reduce() expressions are awkward, but they also illustrate how powerful the function is in its generality: anything that can be computed from a sequence of successive elements can (if awkwardly) be expressed as a reduction.

There are a few common higher-order functions that are not among the “batteries included” with Python, but that are very easy to create as utilities (and are included with many third-party collections of functional programming tools). Different libraries—and other programming languages—may use different names for the utility functions I describe, but analogous capabilities are widespread (as are the names I choose).

Utility Higher-Order Functions

A handy utility is compose(). This is a function that takes a sequence of functions and returns a function that represents the application of each of these argument functions to a data argument:

def compose(*funcs):
    """Return a new function s.t.
       compose(f,g,...)(x) == f(g(...(x)))"""
    def inner(data, funcs=funcs):
        result = data
        for f in reversed(funcs):
            result = f(result)
        return result
    return inner

# >>> times2 = lambda x: x*2
# >>> minus3 = lambda x: x-3
# >>> mod6 = lambda x: x%6
# >>> f = compose(mod6, times2, minus3)
# >>> all(f(i)==((i-3)*2)%6 for i in range(1000000))
# True

For these one-line math operations (times2, minus3, etc.), we could have simply written the underlying math expression at least as easily; but if the composite calculations each involved branching, flow control, complex logic, etc., this would not be true.

The built-in functions all() and any() are useful for asking whether a predicate holds of elements of an iterable. But it is also nice to be able to ask whether any/all of a collection of predicates hold for a particular data item in a composable way. We might implement these as:

all_pred = lambda item, *tests: all(p(item) for p in tests)
any_pred = lambda item, *tests: any(p(item) for p in tests)

To show the use, let us make a few predicates:

>>> is_lt100 = partial(operator.ge, 100)    # less than 100?
>>> is_gt10 = partial(operator.le, 10)      # greater than 10?
>>> from nums import is_prime          # implemented elsewhere
>>> all_pred(71, is_lt100, is_gt10, is_prime)
True
>>> predicates = (is_lt100, is_gt10, is_prime)
>>> all_pred(107, *predicates)
False

The library toolz has what might be a more general version of this called juxt() that creates a function that calls several functions with the same arguments and returns a tuple of results. We could use that, for example, to do:

>>> from toolz.functoolz import juxt
>>> juxt([is_lt100, is_gt10, is_prime])(71)
(True, True, True)
>>> all(juxt([is_lt100, is_gt10, is_prime])(71))
True
>>> juxt([is_lt100, is_gt10, is_prime])(107)
(False, True, True)

The utility higher-order functions shown here are just a small selection to illustrate composability. Look at a longer text on functional programming—or, for example, read the Haskell prelude—for many other ideas on useful utility higher-order-functions.

The operator Module

As has been shown in a few of the examples, every operation that can be done with Python’s infix and prefix operators corresponds to a named function in the operator module. For places where you want to be able to pass a function performing the equivalent of some syntactic operation to some higher-order function, using the name from operator is faster and looks nicer than a corresponding lambda. For example:

# Compare ad hoc lambda with `operator` function
sum1 = reduce(lambda a, b: a+b, iterable, 0)
sum2 = reduce(operator.add, iterable, 0)
sum3 = sum(iterable)    # The actual Pythonic way

The functools Module

The obvious place for Python to include higher-order functions is in the functools module, and indeed a few are in there. However, there are surprisingly few utility higher-order functions in that module. It has gained a few interesting ones over Python versions, but core developers have a resistence to going in the direction of a full functional programming language. On the other hand, as we have seen in a few example above, many of the most useful higher-order functions only take a few lines (sometimes a single line) to write yourself.

Apart from reduce(), which is discussed at the start of this chapter, the main facility in the module is partial(), which has also been mentioned. This operation is called “currying” (after Haskell Curry) in many languages. There are also some examples of using partial() discussed above.

The remainder of the functools module is generally devoted to useful decorators, which is the topic of the next section.

Decorators

Although it is—by design—easy to forget it, probably the most common use of higher-order functions in Python is as decorators. A decorator is just syntax sugar that takes a function as an argument, and if it is programmed correctly, returns a new function that is in some way an enhancement of the original function (or method, or class). Just to remind readers, these two snippets of code defining some_func and other_func are equivalent:

@enhanced
def some_func(*args):
    pass

def other_func(*args):
    pass
other_func = enhanced(other_func)

Used with the decorator syntax, of course, the higher-order function is necessarily used at definition time for a function. For their intended purpose, this is usually when they are best applied. But the same decorator function can always, in principle, be used elsewhere in a program, for example in a more dynamic way (e.g., mapping a decorator function across a runtime-generated collection of other functions). That would be an unusual use case, however.

Decorators are used in many places in the standard library and in common third-party libraries. In some ways they tie in with an idea that used to be called “aspect-oriented programming.” For example, the decorator function asyncio.coroutine is used to mark a function as a coroutine. Within functools the three important decorator functions are functools.lru_cache, functools.total_ordering, and functools.wraps. The first “memoizes” a function (i.e., it caches the arguments passed and returns stored values rather than performing new computation or I/O). The second makes it easier to write custom classes that want to use inequality operators. The last makes it easier to write new decorators. All of these are important and worthwhile purposes, but they are also more in the spirit of making the plumbing of Python programming easier in a general—almost syntactic—way rather than the composable higher-order functions this chapter focuses on.

Decorators in general are more useful when you want to poke into the guts of a function than when you want to treat it as a pluggable component in a flow or composition of functions, often done to mark the purpose or capabilities of a particular function.

This report has given only a glimpse into some techniques for programming Python in a more functional style, and only some suggestions as to the advantages one often finds in aspiring in that direction. Programs that use functional programming are usually shorter than more traditional imperative ones, but much more importantly, they are also usually both more composable and more provably correct. A large class of difficult to debug errors in program logic are avoided by writing functions without side effects, and even more errors are avoided by writing small units of functionality whose operation can be understood and tested more reliably.

A rich literature on functional programming as a general technique—often in particular languages which are not Python—is available and well respected. Studying one of many such classic books, some published by O’Reilly (including very nice video training on functional programming in Python), can give readers further insight into the nitty-gritty of functional programming techniques. Almost everything one might do in a more purely functional language can be done with very little adjustment in Python as well.

Get Functional Programming in Python now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.