Python provides a few in-built commands such as map
, filter
, reduce
as well lambda
(to create anonymous functions) and list comprehension. These are typical commands from functional languages of which LISP is probably best known.
Functional programming can be extremely powerful and one of the strengths of Python is that it allows to program using (i) imperative/procedural programming style, (ii) object oriented style and (iii) functional style. It is the programmers choice which tools to select from which style and how to mix them to best address a given problem.
In this chapter, we provide some examples for usage of the commands listed above.
All functions we have seen in Python so far have been defined through the def
keyword, for example:
def f(x):
return x ** 2
This funtion has the name f
. Once the function is defined (i.e. the Python interpreter has come across the def
line), we can call the function using its name, for example
y = f(6)
Sometimes, we need to define a function that is only used once, or we want to create a function but don’t need a name for it (as for creating closures). In this case, this is called anonymous function as it does not have a name. In Python, the lambda
keyword can create an anonymous function.
We create a (named) function first, check it’s type and behaviour:
def f(x):
return x ** 2
f
type(f)
f(10)
Now we do the same with an anonymous function:
lambda x: x ** 2
type(lambda x: x ** 2)
(lambda x: x ** 2)(10)
This works exactly in the same way but – as the anonymous function does not have a name – we need to define the function (through the lambda
expression) – every time we need it.
Anonymous functions can take more than one argument:
(lambda x, y: x + y)(10, 20)
(lambda x, y, z: (x + y) * z )(10, 20, 2)
We will see some examples using lambda
which will clarify typical use cases.
The map function lst2 = map(f, s )
applies a function f
to all elements in a sequence s
.
The result of map
can be turned into a list with the same length as s
:
def f(x):
return x ** 2
lst2 = list(map(f, range(10)))
lst2
list(map(str.capitalize, ['banana', 'apple', 'orange']))
Often, this is combined with the anonymous function lambda
:
list(map(lambda x: x ** 2, range(10) ))
list(map(lambda s: s.capitalize(), ['banana', 'apple', 'orange']))
The filter function lst2 = filter( f, lst)
applies the function f
to all elements in a sequence s
. The function f
should return True
or False
. This makes a list which will contain only those elements s*i* of the sequence s
for which f
(s*i*) has returned True
.
def greater_than_5(x):
if x > 5:
return True
else:
return False
list(filter(greater_than_5, range(11)))
The usage of lambda
can simplify this significantly:
list(filter(lambda x: x > 5, range(11)))
known_names = ['smith', 'miller', 'bob']
list(filter( lambda name : name in known_names, \
['ago', 'smith', 'bob', 'carl']))
List comprehensions provide a concise way to create and modify lists without resorting to use of map(), filter() and/or lambda. The resulting list definition tends often to be clearer than lists built using those constructs. Each list comprehension consists of an expression followed by a for
clause, then zero or more for
or if
clauses. The result will be a list resulting from evaluating the expression in the context of the for and if clauses which follow it. If the expression would evaluate to a tuple, it must be parenthesized.
Some examples will make this clearer:
freshfruit = [' banana', ' loganberry ', 'passion fruit ']
[weapon.strip() for weapon in freshfruit]
vec = [2, 4, 6]
[3 * x for x in vec]
[3 * x for x in vec if x > 3]
[3 * x for x in vec if x < 2]
[[x, x ** 2] for x in vec]
We can also use list comprehension to modify the list of integers returned by the range
command so that our subsequent elements in the list increase by non-integer fractions:
[x*0.5 for x in range(10)]
Let’s now revisit the examples from the section on filter
[x for x in range(11) if x>5 ]
[name for name in ['ago','smith','bob','carl'] \
if name in known_names]
and the examples from the map
section
[x ** 2 for x in range(10) ]
[fruit.capitalize() for fruit in ['banana', 'apple', 'orange'] ]
all of which can be expressed through list comprehensions.
More details
The reduce
function takes a binary function f(x,y)
, a sequence s
, and a start value a0
. It then applies the function f
to the start value a0
and the first element in the sequence: a1 = f(a,s[0])
. The second element (s[1]
) of the sequence is then processed as follows: the function f is called with arguments a1
and s[1]
, i.e. a2 = f(a1,s[1])
. In this fashion, the whole sequence is processed. Reduce returns a single number.
This can be used, for example, to compute a sum of numbers in a sequence if the function f(x,y)
returns x+y
:
from functools import reduce
def add(x,y):
return x+y
reduce(add, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 0)
reduce(add, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 100)
We can modify the function add
to provide some more detail about the process:
def add_verbose(x, y):
print("add(x=%s, y=%s) -> %s" % (x, y, x+y))
return x+y
reduce(add_verbose, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 0)
It may be instructive to use an asymmetric function f
, such as add_len( n, s )
where s is a sequence and the function returns n+len(s)
(suggestion from Thomas Fischbacher):
def add_len(n,s):
return n+len(s)
reduce(add_len, ["This","is","a","test."],0)
As before, we’ll use a more verbose version of the binary function to see what is happening:
def add_len_verbose(n,s):
print("add_len(n=%d, s=%s) -> %d" % (n, s, n+len(s)))
return n+len(s)
reduce(add_len_verbose, ["This", "is", "a", "test."], 0)
Another way to understand what the reduce function does is to look at the following function (kindly provided by Thomas Fischbacher) which behaves like reduce
but explains what it does:
Here is an example using the explain_reduce
function:
cd code/
from explain_reduce import explain_reduce
def f(a,b):
return a+b
reduce(f, [1,2,3,4,5], 0)
explain_reduce(f, [1,2,3,4,5], 0)
Reduce is often combined with lambda
:
reduce(lambda x,y:x+y, [1,2,3,4,5], 0)
There is also the operator
module which provides standard Python operators as functions. For example, the function operator.__add__(a,b)
is executed when Python evaluates code such as a+b
. These are generally faster than lambda
expressions. We could write the example above as
import operator
reduce(operator.__add__, [1,2,3,4,5], 0)
Use help(’operator’)
to see the complete list of operator functions.
Let’s compare the example introduced at the beginning of the chapter written (i) using a for-loop and (ii) list comprehension. Again, we want to compute the numbers 02, 12, 22, 32, ... up to (n − 1)2 for a given n.
Implementation (i) using a for-loop with n=10:
y = []
for i in range(10):
y.append(i ** 2)
Implementation (ii) using list comprehension:
y = [x ** 2 for x in range(10)]
or using map
:
y = map(lambda x: x ** 2, range(10))
The versions using list comprehension and map
fit into one line of code whereas the for-loop needs 3. This example shows that functional code result in very concise expressions. Typically, the number of mistakes a programmer makes is per line of code written, so the fewer lines of code we have, the fewer bugs we need to find.
Often programmers find that initially the list-processing tools introduced in this chapter seem less intuitive than using for-loops to process every element in a list individually, but that – over time – they come to value a more functional programming style.
The functional tools described in this chapter can also be faster than using explicit (for or while) loops over list elements.
The program list_comprehension_speed.py
below computes $\\sum\_{i=0}^{N-1} i^2$ for a large value of N using 4 different methods and records execution time:
Method 1: for-loop (with pre-allocated list, storing of i2 in list, then using in-built sum
function)
Method 2: for-loop without list (updating sum as the for-loop progresses)
Method 3: using list comprehension
Method 4: using numpy. (Numpy is covered in chapter 14)
The program produces the following output:
!python list_comprehension_speed.py
The actual execution performance depends on the computer. The relative performance may depend on versions of Python and its support libraries (such as numpy) we use.
With the current version (python 3.4, numpy 1.10, on a x84 machine running OS X), we see that methods 1 and 2 (for-loop without list and with pre-allocated list) are slowest, somewhat closely followed by the slightly faster list comprehension. The fastest method is number 4 (using numpy).
For reference, here is the source code of the programme:
!cat list_comprehension_speed.py