15. Functional Programming in Python

Programming styles

Programming languages support decomposing problems in several different ways:

  • Most programming languages are procedural: programs are lists of instructions that tell the computer what to do with the program’s input.

    • C, Pascal, and even Unix shells are procedural languages.
  • In declarative languages, you write a specification that describes the problem to be solved, and the language implementation figures out how to perform the computation efficiently.

    • SQL is the declarative language you’re most likely to be familiar with; a SQL query describes the data set you want to retrieve, and the SQL engine decides whether to scan tables or use indexes, which subclauses should be performed first, etc.
  • Object-oriented programs manipulate collections of objects. Objects have internal state and support methods that query or modify this internal state in some way.

    • Smalltalk and Java are object-oriented languages. C++ and Python are languages that support object-oriented programming, but don’t force the use of object-oriented features.
  • Functional programming decomposes a problem into a set of functions. Ideally, functions only take inputs and produce outputs, and don’t have any internal state that affects the output produced for a given input.

    • Well-known functional languages include the ML family (Standard ML, OCaml, and other variants) and Haskell.

What is Functional Programming

  • In a functional program, input flows through a set of functions. Each function operates on its input and produces some output.

  • Functional style discourages functions with side effects that modify internal state or make other changes that aren’t visible in its return value. Functions that have no side effects at all are called purely functional.

  • Avoiding side effects means not using data structures that get updated as a program runs; every function’s output must only depend on its input.

More about functional programming

  • Recursion is used as a primary control structure.

  • There is a focus on list processing. Lists are often used with recursion on sublists as a substitute for loops.

  • Functional programming worries about what is to be computed rather than how it is to be computed.

  • Much functional programming utilizes “higher order” functions (in other words, functions that operate on functions that operate on functions).

Python Functional Programming

iterator

An iterator is an object representing a stream of data; this object returns the data one element at a time.

A Python iterator must support a method called __next__() that takes no arguments and always returns the next element of the stream.

If there are no more elements in the stream, __next__() must raise the StopIteration exception.

In [ ]:
L = [1,2,3]
it = iter(L)
print(it, type(it))  
print(it.__next__())  # same as next(it)
print(next(it))
print(next(it))
print(next(it))

Python expects iterable objects in several different contexts, the most important being the for statement.

In the statement for X in Y, Y must be an iterator or some object for which iter() can create an iterator.

These two statements are equivalent:

for i in iter(obj):
    print(i)

for i in obj:
    print(i)

Iterators can be materialized as lists or tuples:

In [ ]:
L = [1,2,3]

iterator = iter(L)

t = tuple(iterator)

print(t)

Sequence unpacking also supports iterators:

In [ ]:
L = [1,2,3]
iterator = iter(L)
a,b,c = iterator
a,b,c

Data Types That Support Iterators

Any Python sequence type, such as strings, will automatically support creation of an iterator.

In [ ]:
m = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 
     'May': 5, 'Jun': 6, 'Jul': 7, 'Aug': 8, 
     'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}

for key in m:  
    print(key, m[key])
In [ ]:
L = [('Italy', 'Rome'), ('France', 'Paris'), 
     ('US', 'Washington DC')]

print(dict(iter(L)) == dict(L)) 
In [ ]:
S = {2, 3, 5, 7, 11, 13}
for i in S:
    print(i)

List comprehensions

Two common operations on an iterator’s output are

1) performing some operation for every element,

2) selecting a subset of elements that meet some condition.

For example, given a list of strings, you might want to strip off trailing whitespace from each line:

In [ ]:
line_list = ['  line 1\n', 'line 2  \n', '']

# List comprehension -- returns list
stripped_list = [line.strip() for line in line_list]

print(stripped_list)
In [ ]:
stripped_list = [line.strip() for line in line_list
                 if line != ""]

print(stripped_list)
In [ ]:
seq1 = 'abc'
seq2 = (1,2,3)

[(x, y) for x in seq1 for y in seq2]

Built-in functions: map

map(f, iterA, iterB, ...) returns an iterator over the sequence

f(iterA[0], iterB[0]), f(iterA[1], iterB[1]), f(iterA[2], iterB[2]), ....

In [ ]:
def upper(s):
    return s.upper()

list(map(upper, ['sentence', 'fragment']))

You can of course achieve the same effect with a list comprehension.

In [ ]:
[upper(s) for s in ['sentence', 'fragment']]

Built-in functions: filter

filter(predicate, iter) returns an iterator over all the sequence elements that meet a certain condition, and is similarly duplicated by list comprehensions.

A predicate is a function that returns the truth value of some condition; for use with filter(), the predicate must take a single value.

In [ ]:
def is_even(x):
    return (x % 2) == 0

list(filter(is_even, range(10)))

This can also be written as a list comprehension:

In [ ]:
[x for x in range(10) if is_even(x)]
In [ ]:
for x in filter(is_even, range(10)):
    print(x)

Built-in functions: sorted

sorted(iterable, key=None, reverse=False) collects all the elements of the iterable into a list, sorts the list, and returns the sorted result.

The key and reverse arguments are passed through to the constructed list’s sort() method.

In [ ]:
import random
# Generate 8 random numbers between [0, 10000)
rand_list = random.sample(range(10000), 8)
print(rand_list) 
print(sorted(rand_list)) 
print(sorted(rand_list, reverse=True))

Built-in functions: zip

zip(iterA, iterB, ...) takes one element from each iterable and returns them in a tuple:

In [ ]:
list(zip(['a', 'b', 'c'], [1, 2, 3]))

Built-in functions: all and zip

The any(iter) and all(iter) built-ins look at the truth values of an iterable’s contents.

any() returns True if any element in the iterable is a true value, and all() returns True if all of the elements are true values:

In [ ]:
print(any([0,1,0]))
print(any([0,0,0]))
print(any([1,1,1]))
print(all([0,1,0]))
print(all([0,0,0]))
print(all([1,1,1]))

The itertools module

The itertools module contains a number of commonly-used iterators as well as functions for combining several iterators.

Creating new iterators

count, repeat, cycle, chain

itertools.count(n) returns an infinite stream of integers, increasing by 1 each time.

In [ ]:
import itertools
import random

rand = random.randint(0,10000)
cnt = 0
for f in itertools.count() :
    cnt +=1
    if f == rand:
        break

print('guess {} times'.format(cnt))

combinations

The itertools.combinations(iterable, r) returns an iterator giving all possible r-tuple combinations of the elements contained in iterable.

itertools.permutations(iterable, r=None) returning all possible arrangements of length r:

In [ ]:
list(itertools.combinations([1, 2, 3, 4, 5], 2))
In [ ]:
list(itertools.permutations([1, 2, 3, 4, 5], 2))
In [ ]:
# Another eight queens problem solution

from itertools import permutations

SIZE = 8
cols = range(SIZE)
for vec in permutations(cols):
    if (SIZE == len(set(vec[i]+i for i in cols))
          == len(set(vec[i]-i for i in cols))):
        print(vec)

The functools module

The functools module contains some higher-order functions.

A higher-order function takes one or more functions as input and returns a new function.

functools.partial

For programs written in a functional style, you’ll sometimes want to construct variants of existing functions that have some of the parameters filled in.

Consider a Python function f(a, b, c); you may wish to create a new function g(b, c) that’s equivalent to f(1, b, c); you’re filling in a value for one of f()‘s parameters. This is called “partial function application”.

In [ ]:
import functools

def log(message, subsystem):
    """Write the contents of 'message' to the specified subsystem."""
    print('%s: %s' % (subsystem, message))
    pass

server_log = functools.partial(log, subsystem='server')
server_log('Unable to open socket')

functools.reduce

functools.reduce(func, iter, [initial_value]) cumulatively performs an operation on all the iterable’s elements and, therefore, can’t be applied to infinite iterables.

func must be a function that takes two elements and returns a single value.

functools.reduce() takes the first two elements A and B returned by the iterator and calculates func(A, B). It then requests the third element, C, calculates func(func(A, B), C), combines this result with the fourth element returned, and continues until the iterable is exhausted.

If the iterable returns no values at all, a TypeError exception is raised. If the initial value is supplied, it’s used as a starting point and func(initial_value, A) is the first calculation.

In [ ]:
import operator, functools

functools.reduce(operator.concat, ['A', 'BB', 'C'])
In [ ]:
import operator, functools

functools.reduce(operator.mul, [1,2,3])
In [ ]:
import operator, functools

functools.reduce(operator.add, [1,2,3,4])

The lambda expression

When writing functional-style programs, you’ll often need little functions that act as predicates or that combine elements in some way.

If there’s a Python built-in or a module function that’s suitable, you don’t need to define a new function.

If the function you need doesn’t exist, you need to write it.

One way to write small functions is to use the lambda statement.

lambda takes a number of parameters and an expression combining these parameters, and creates an anonymous function that returns the value of the expression

In [ ]:
adder = lambda x, y: x+y

adder(1,2)
In [ ]:
import functools

functools.reduce(lambda x,y:x*y, [1,2,3])
In [ ]:
import functools

functools.reduce(lambda x,y:x+y, [1,2,3,4])
In [ ]:
list(map(lambda x: x.upper(), 
         ['sentence', 'fragment']))
In [ ]:
list(filter(lambda x: x%2 == 0, range(10)))
In [ ]:
from functools import reduce 

def factorial(n):
    return reduce(lambda x,y: x*y, range(1, n+1))

factorial(100)
In [ ]:
score = {'A':20,'B':15, 'C':60, 'D':40}
sorted(score.items(), key=lambda kv:kv[1])

Exercise

  1. Sort list l, treating items as integers or strings
l = [28,14,'28', 5,'9', '1', 0, 6, '23',19]
In [ ]:
l = [28,14,'28', 5,'9', '1', 0, 6, '23',19]
print(sorted(l, key=int))
In [ ]:
l = [28,14,'28', 5,'9', '1', 0, 6, '23',19]
print(sorted(l, key=str))
  1. Write a reduce func, the same as functools.reduce
def reduce(function, iterable, initializer=None)
In [ ]:
def reduce(function, iterable, initializer=None):
    it = iter(iterable)
    if initializer is None:
        value = next(it)
    else:
        value = initializer
    for element in it:
        value = function(value, element)
    return value

reduce(lambda x,y:x+y, [1,2,3])