Functional Python

A friend of mine is learning Python and was curious about functional programming, so I have written this brief tutorial. Python isn’t a full-fledged functional language, but it supports some very useful functional idioms.

It’s best to approach this tutorial by “programming along” at the Python interactive prompt. Try typing everything in to see what the results are. — Scott Moonen

Introduction

Imagine you have a list of numbers and want to filter out all even numbers:

q = [1,2,5,6,8,12,15,17,20,23,24]

def is_even(x) :
  return x % 2 == 0

result = filter(is_even, q)

(Remember that the “%” operator is the “modulus” or “remainder” operator. If the remainder when a number is divided by two is zero, then the number must be even.)

If we only use is_even once, it’s kind of annoying that we have to define it. Wouldn’t it be nice if we could just define it inside of the call to filter? We want to do something like “q = filter(x % 2 == 0, q)”, but that won’t quite work, since the “x % 2 == 0” is evaluated before the call to filter. We want to pass a function to filter, not an expression.

We can do this using the lambda operator. lambda allows you to create an unnamed throw-away function. Try this:

q = [1,2,5,6,8,12,15,17,20,23,24]
result = filter(lambda x : x % 2 == 0, q)

lambda tells Python that a function follows. Before the colon (:) you list the parameters of the function (in this case, only one, x). After the colon you list what the function returns. This function doesn’t have a name, and Python disposes of it as soon as filter is done with it.

You can, however, assign the function to a variable to give it a name. The following two statements are identical:

def is_odd(x) :
  return x % 2 == 1

is_odd = lambda x : x % 2 == 1

map

Here’s another example. The map function applys a function to every item in a list, and returns the result. This example adds 1 to every element in the list:

result = map(lambda x : x + 1, [1,2,3,4,5])

(At this point, result holds [2,3,4,5,6].)

The map function can also process more than one list at a time, provided they are the same length. This allows you to do things like add all of the elements in a pair of lists. Note that our lambda-function here has two parameters:

result = map(lambda x,y : x+y, [1,2,3,4,5], [6,7,8,9,10])

(At this point, result holds [7, 9, 11, 13, 15].)

reduce

Python also provides the reduce function. This is a bit more complicated; you provide it a list and a function, and it reduces that list by applying the function to pairs of elements in the list. An example is the best way to understand this. Let’s say we want to find the sum of all the elements in a list:

sum = reduce(lambda x,y : x+y, [1,2,3,4,5])

reduce will first apply the function to 1,2, yielding 3. It will then apply the function to 3,3 (the first 3 is the result of adding 1+2), yielding 6. It will then apply the function to 6,4 (the 6 is the result of adding 3+3), yielding 10. Finally, it will apply the function to 10,5, yielding 15. The result stored in sum is 15.

Similarly, we can find the cumulative product of all items in a list. The following example stores 120 (=1*2*3*4*5) in product:

product = reduce(lambda x,y : x*y, [1,2,3,4,5])

Conditionals

The words lambda and lambda-function are used for historical reasons. Functional programming is not entirely synonymous with lambda-functions, but lambda-functions (and tools like filter, map, and reduce) are a significant aspect of functional programming. Python doesn’t support everything typical of a functional programming language, but it supports enough that it is useful.

The one limitation that most disappoints me is that Python lacks is a functional way of writing if/else. Sometimes you just want to do something like this:

lambda x : if_else(x>100, “big number”, “little number”)

(This would return the string “big number” if x was greater than 100, and “little number” otherwise.) Sometimes I get around this by defining my own if_else that I can use in lambda-functions:

def if_else(condition, a, b) :
   if condition : return a
   else         : return b

The only disadvantage here is that if you use if_else, both expressions are evaluated (i.e., this performs “strict evaluation” rather than “lazy evaluation”). The following code looks like it guards against exceeding a list’s boundaries, but it will actually generate an exception when the boundaries are exceeded, because the value of q[index] will always be computed even if the condition is false:

safe_lookup = lambda index : if_else(index >= 0 and index < len(q), q[index], None)

There are ways around this, but they are not very elegant. If you need to perform lazy evaluation, you should write an ordinary function with ordinary conditional statements:

def safe_lookup(index) :
   if index >= 0 and index < len(q) : return q[index]
   else                             : return None

Background

One of the advantages of lambda-functions is that they allow you to write very concise code. A result of this, however, is that the code is literally “dense” with meaning, and it can be hard to read if you are not accustomed to functional programming. In our first example, “filter(is_even, q)” was fairly easy to understand (fortunately, we chose a descriptive function name), while “filter(lambda x : x % 2 == 0, q)” takes a little longer to comprehend.

If you are a masochistic mathematical geek, you’ll probably enjoy this (I do). If not, you might still prefer to break things into smaller pieces by defining all of your functions first and then using them by name. That’s ok! Functional programming isn’t for everyone.

Another advantage of functional programming is that it corresponds very closely to the mathematical notion of functions. Note that our lambda-functions didn’t store any results into variables, or have any other sort of side effect. This is not simply “nifty”. Functions without side effects cause fewer bugs, because their behavior is deterministic (this is related to the dictum that you aren’t supposed to use global variables). It is also much easier to mathematically prove that such functions do what you think they are doing. (Note that it’s still possible to write a lambda function that has side effects, if the lambda function calls another function that has side effects; for example “lambda x : sys.stdout.write(str(x))” has the side effect of printing its parameter to the screen.)

Some functional languages are also homoiconic. This means that not only can you create pure functions using something like lambda, but you can also manipulate them (e.g., reach inside a function and change something it does). Python is not homoiconic.


For the masochistic, consider some of my PythonSpikes. Not all of these use the functional idiom, but some do.

Visit my other blogs: truth, adorned, finance and stuff.
See Gospel Software for online church administration tools
See GuestView for managing church guest or visitor follow up
See Group Sched for potluck and meal planning