Functional Tools in Python

A brief introduction to Functional Programming in Python.

What is Functional Programming?

From (Wikipedia)[https://en.wikipedia.org/wiki/Functional_programming]:

...Functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids state and mutable data...Eliminating side effects, i.e., changes in state that do not depend on the function inputs, can make it much easier to understand and predict the behavior of a program, which is one of the key motivations for the development of functional programming.

What are Some Functional Languages?

Popular functional programming languages:

  • Scheme
  • Erlang
  • OCaml
  • Haskell
  • F#
  • Clojure
  • R

Disclaimer

Who Cares?

Testing and debugging functional programs tend to be easier. There is no global state and the program is a sequence of function calls. Functions tend to be small and compartmentalized. Each function can be tested individually. Debugging involves checking the input and output of each function call.

Functional programming/languages have been growing in popularity in industry and knowing these languages/concepts makes for a rare marketable skill.

Also it's fun.

Some Notes on Function Parameters

There was a mention about not passing an empty list as a default argument. This is not only true of lists but any mutable type such as a set or a dictionary.

This is because the function defaults are evaluated when the function is defined. If the default type is mutable then it can be changed on subsequent function calls.

In [10]:
def simple(x, y=[]):
    y.append(x)
    return y
In [11]:
simple.__defaults__
Out[11]:
([],)
In [12]:
simple(1)
Out[12]:
[1]
In [13]:
simple(2)
Out[13]:
[1, 2]
In [14]:
simple.__defaults__
Out[14]:
([1, 2],)

Getting Rid of Logical Blocks

Small logical blocks can often be removed by making use of Python short circuiting logical statements.

In [15]:
x = 2
x_place = 'did not place'
if x == 1:
    x_place = 'first'
elif x == 2:
    x_place = 'second'
x_place
Out[15]:
'second'
In [16]:
x = 2
(x == 1 and 'first') or (x == 2 and 'second') or 'did not place'
Out[16]:
'second'

Using Short Circuit For Good

One of the best uses of this logical evaluation is handling default arguments that you want to be mutable types such as lists or dictionaries.

In [17]:
def simple(x=None):
    x = x or {}
    # Do some more work

Getting Rid of Loops

List comprehensions allow you to build loops in place. Simple loops can be removed with list comprehensions. Now Python supports dictionary and set comprehensions as well.

In [18]:
mix = [1, 'a', 'b', 2, 4]
letters = []
for x in mix:
    if isinstance(mix, str):
        letters.append(x)
letters
Out[18]:
[]
In [19]:
[x for x in mix if isinstance(x, str)]
Out[19]:
['a', 'b']
In [20]:
def identity(n):
    return [[1 if i == j else 0 for i in range(n)] for j in range(n)]

identity(3)
Out[20]:
[[1, 0, 0], [0, 1, 0], [0, 0, 1]]
In [21]:
[(i, j) for i in range(3) for j in range(3)]
Out[21]:
[(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)]

Anonymous Functions

So far we have always defined functions using the def keyword. You can also define anonymous (unnamed) functions with lambda.

In [22]:
f = lambda x: x * x # Create a new function and assign to f
f.__class__
Out[22]:
function
In [23]:
f(2)
Out[23]:
4

More List Filtering

You can also use the filter function. The first argument is the function to be used for the filtering. This could be a defined function or a lambda. The second argument is the iterable. It returns the filtered list.

In [24]:
mix = [1, 'a', 'b', 2, 4]
letters = filter(lambda x: isinstance(x, str), mix)
letters
Out[24]:
<filter at 0x7f2d50c50f60>
In [25]:
list(letters)
Out[25]:
['a', 'b']

Getting Rid of More Loops

List filtering isn't the only type of lists you can remove. The reduce function applies a binary function in sequence to an iterable.

In [26]:
import functools
# This is equivalent to ((1 + 2) + 3) + 4)
functools.reduce(lambda x, y: x + y, [1, 2, 3, 4])
Out[26]:
10
In [27]:
# This is equivalent to ((1 * 2) * 3) * 4) or 4!
functools.reduce(lambda x, y: x * y, [1, 2, 3, 4])
Out[27]:
24

Map

map applies a function to each item in an iterable and returns a list of the results.

In [28]:
# Create list of the first four squares
squares = []
for x in [1, 2, 3, 4]:
    squares.append(x**2)
squares
Out[28]:
[1, 4, 9, 16]
In [29]:
# Becomes
squares = map(lambda x: x**2, [1, 2, 3, 4])
In [30]:
squares
Out[30]:
<map at 0x7f2d50c524e0>
In [31]:
list(squares)
Out[31]:
[1, 4, 9, 16]
In [32]:
text = '1,0,0;0,1,0;0,0,1'
rows = text.split(';')
matrix = []
for row in rows:
    matrix.append([float(x) for x in row.split(',')])
matrix
Out[32]:
[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
In [33]:
matrix = map(lambda x: map(float, x.split(',')), text.split(';'))

Sorting Lists

The built-in list type has a sort method which sorts the list in place. sort can also take a key parameter which is a function which takes one parameter returns the value which will be used for the sorting. Passing reverse=True will reverse the sort order.

In [34]:
test = [3, 4, 5, 1, 2]
test.sort()
test
Out[34]:
[1, 2, 3, 4, 5]
In [35]:
test = [3, 4, 5, 1, 2]
test.sort(key=lambda x: x % 3)
test
Out[35]:
[3, 4, 1, 5, 2]
In [36]:
test = ['C', 'a', 'd', 'B', 'E']
test.sort()
test # Default alpha sort
Out[36]:
['B', 'C', 'E', 'a', 'd']
In [37]:
test.sort(key=str.lower)
test # Case insensitive alpha sort
Out[37]:
['a', 'B', 'C', 'd', 'E']
In [38]:
test = [(1, 2), (5, 3), (7, 1), (3, 8)]
test.sort(key=lambda x: x[1])
test # Sort by second entry
Out[38]:
[(7, 1), (1, 2), (5, 3), (3, 8)]

Sorting Iterables

list.sortis great but what if you need to sort things that are iterables but not lists. For that you can use the built-in sorted function. The first argument is an iterable and the remaining arguments are the same as sort. Unlike sort which returns None, sorted returns the sorted items as a list.

In [39]:
sorted((8, 6, 7, 5, 3, 0, 9))
Out[39]:
[0, 3, 5, 6, 7, 8, 9]
In [40]:
sorted('jenny')
Out[40]:
['e', 'j', 'n', 'n', 'y']
In [41]:
sorted({'got': 'your number', 'need': 'make you mine'})
Out[41]:
['got', 'need']

Common Key Functions

There are some common key functions:

  • Sorting a list of tuples (or nested list) by a given index
  • Sorting by an attribute
  • Sorting by a method

While it isn't difficult to write these functions you can instead use the operator module.

In [42]:
from operator import itemgetter, attrgetter, methodcaller

test = [(1, 2), (5, 3), (7, 1), (3, 8)]
sorted(test, key=itemgetter(1))
Out[42]:
[(7, 1), (1, 2), (5, 3), (3, 8)]
In [43]:
test = ['C', 'a', 'd', 'B', 'E']
sorted(test, key=methodcaller('lower'))
Out[43]:
['a', 'B', 'C', 'd', 'E']
In [44]:
test = [1 +  1j, 2 - 1j, -1 + 2j]
sorted(test, key=attrgetter('real'))
Out[44]:
[(-1+2j), (1+1j), (2-1j)]

itertools Module

The itertools module defines functions for commonly used iterators.

  • Lists of integers
  • Cycles of sequences
  • Chains of iterators
  • Permutaions of a sequence
  • Combinations of a sequence
In [45]:
from itertools import chain, takewhile, dropwhile
from itertools import combinations, permutations

a = [1, 2, 3]
b = 'abc'
[(x, type(x)) for x in chain(a, b)]
Out[45]:
[(1, int), (2, int), (3, int), ('a', str), ('b', str), ('c', str)]
In [46]:
[u''.join(x) for x in combinations(b, 2)]
Out[46]:
['ab', 'ac', 'bc']
In [47]:
[u''.join(x) for x in permutations(b, 2)]
Out[47]:
['ab', 'ac', 'ba', 'bc', 'ca', 'cb']
In [48]:
list(takewhile(lambda x: x % 2 == 1, a))
Out[48]:
[1]
In [49]:
list(dropwhile(lambda x: x in 'aeiou', b))
Out[49]:
['b', 'c']

Closures

Closures are combination of a function and an environment. They are named closures because they are said to "close over" free variables that are passed when defining the closure.

Since I'm sure none of that made any sense let's look at some examples.

In [50]:
def greater_than_bound(bound):
    def greater(x):
        return x > bound
    return greater

greater_than_ten = greater_than_bound(10)
greater_than_ten(20)
Out[50]:
True
In [51]:
greater_than_ten(2)
Out[51]:
False

Why Closures?

Why in the world would you want to do this? Somethings are just too complicated for lambda. lambda expressions cannot contain if, for or while statements. Though, as we have seen, many of these statements can be replaced.

Up Next

A tour of the Python standard library.