A brief introduction to Functional Programming in Python.
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.
Popular functional programming languages:
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.
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.
def simple(x, y=[]):
y.append(x)
return y
simple.__defaults__
([],)
simple(1)
[1]
simple(2)
[1, 2]
simple.__defaults__
([1, 2],)
Small logical blocks can often be removed by making use of Python short circuiting logical statements.
x = 2
x_place = 'did not place'
if x == 1:
x_place = 'first'
elif x == 2:
x_place = 'second'
x_place
'second'
x = 2
(x == 1 and 'first') or (x == 2 and 'second') or 'did not place'
'second'
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.
def simple(x=None):
x = x or {}
# Do some more work
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.
mix = [1, 'a', 'b', 2, 4]
letters = []
for x in mix:
if isinstance(mix, str):
letters.append(x)
letters
[]
[x for x in mix if isinstance(x, str)]
['a', 'b']
def identity(n):
return [[1 if i == j else 0 for i in range(n)] for j in range(n)]
identity(3)
[[1, 0, 0], [0, 1, 0], [0, 0, 1]]
[(i, j) for i in range(3) for j in range(3)]
[(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)]
So far we have always defined functions using the def
keyword. You can also define anonymous (unnamed) functions with lambda
.
f = lambda x: x * x # Create a new function and assign to f
f.__class__
function
f(2)
4
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.
mix = [1, 'a', 'b', 2, 4]
letters = filter(lambda x: isinstance(x, str), mix)
letters
<filter at 0x7f2d50c50f60>
list(letters)
['a', 'b']
List filtering isn't the only type of lists you can remove. The reduce
function applies a binary function in sequence to an iterable.
import functools
# This is equivalent to ((1 + 2) + 3) + 4)
functools.reduce(lambda x, y: x + y, [1, 2, 3, 4])
10
# This is equivalent to ((1 * 2) * 3) * 4) or 4!
functools.reduce(lambda x, y: x * y, [1, 2, 3, 4])
24
map
applies a function to each item in an iterable and returns a list of the results.
# Create list of the first four squares
squares = []
for x in [1, 2, 3, 4]:
squares.append(x**2)
squares
[1, 4, 9, 16]
# Becomes
squares = map(lambda x: x**2, [1, 2, 3, 4])
squares
<map at 0x7f2d50c524e0>
list(squares)
[1, 4, 9, 16]
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
[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
matrix = map(lambda x: map(float, x.split(',')), text.split(';'))
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.
test = [3, 4, 5, 1, 2]
test.sort()
test
[1, 2, 3, 4, 5]
test = [3, 4, 5, 1, 2]
test.sort(key=lambda x: x % 3)
test
[3, 4, 1, 5, 2]
test = ['C', 'a', 'd', 'B', 'E']
test.sort()
test # Default alpha sort
['B', 'C', 'E', 'a', 'd']
test.sort(key=str.lower)
test # Case insensitive alpha sort
['a', 'B', 'C', 'd', 'E']
test = [(1, 2), (5, 3), (7, 1), (3, 8)]
test.sort(key=lambda x: x[1])
test # Sort by second entry
[(7, 1), (1, 2), (5, 3), (3, 8)]
list.sort
is 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.
sorted((8, 6, 7, 5, 3, 0, 9))
[0, 3, 5, 6, 7, 8, 9]
sorted('jenny')
['e', 'j', 'n', 'n', 'y']
sorted({'got': 'your number', 'need': 'make you mine'})
['got', 'need']
There are some common key functions:
While it isn't difficult to write these functions you can instead use the operator
module.
from operator import itemgetter, attrgetter, methodcaller
test = [(1, 2), (5, 3), (7, 1), (3, 8)]
sorted(test, key=itemgetter(1))
[(7, 1), (1, 2), (5, 3), (3, 8)]
test = ['C', 'a', 'd', 'B', 'E']
sorted(test, key=methodcaller('lower'))
['a', 'B', 'C', 'd', 'E']
test = [1 + 1j, 2 - 1j, -1 + 2j]
sorted(test, key=attrgetter('real'))
[(-1+2j), (1+1j), (2-1j)]
itertools
Module¶The itertools
module defines functions for commonly used iterators.
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)]
[(1, int), (2, int), (3, int), ('a', str), ('b', str), ('c', str)]
[u''.join(x) for x in combinations(b, 2)]
['ab', 'ac', 'bc']
[u''.join(x) for x in permutations(b, 2)]
['ab', 'ac', 'ba', 'bc', 'ca', 'cb']
list(takewhile(lambda x: x % 2 == 1, a))
[1]
list(dropwhile(lambda x: x in 'aeiou', b))
['b', 'c']
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.
def greater_than_bound(bound):
def greater(x):
return x > bound
return greater
greater_than_ten = greater_than_bound(10)
greater_than_ten(20)
True
greater_than_ten(2)
False
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.
A tour of the Python standard library.