Functional Programming in Python

Shagun Sodhani

Functions as first class citizens


In [1]:
def mul(a, b):
    return a*b

mul(2, 3)


Out[1]:
6

In [2]:
mul = lambda a, b: a*b

mul(2, 3)


Out[2]:
6

Lambda is another way of defining a function

Higher Order Functions

# Functions as arguments to other functions

In [3]:
mul(mul(2, 3), 3)


Out[3]:
18

In [4]:
def transform_and_add(func, a, b):
    return func(a) + func(b)

transform_and_add(lambda x: x**2, 1, 2)


Out[4]:
5

Why would I want something like this?

A Familiar Pattern


In [5]:
def square_and_add(a, b):
    return (a**2 + b**2)

def cube_and_add(a, b):
    return (a**3 + b**3)

def quad_and_add(a, b):
    return (a**4 + b**4)

print(square_and_add(1, 2))
print(cube_and_add(1, 2))
print(quad_and_add(1, 2))


5
9
17

In [6]:
square = lambda x: x**2
cube = lambda x: x**3
quad = lambda x: x**4
print(square_and_add(1, 2) == transform_and_add(square, 1, 2))
print(cube_and_add(1, 2) == transform_and_add(cube, 1, 2))
print(quad_and_add(1, 2) == transform_and_add(quad, 1, 2))


True
True
True

In [7]:
def square_and_add(a, b):
    return (a**2 + b**2)

def cube_and_mul(a, b):
    return ((a**3) * (b**3))

def quad_and_div(a, b):
    return ((a**4) / (b**4))

print(square_and_add(1, 2))
print(cube_and_mul(1, 2))
print(quad_and_div(1, 2))


5
8
0.0625

In [8]:
def transform_and_reduce(func_transform, func_reduce, a, b):
    return func_reduce(func_transform(a), func_transform(b))

print(square_and_add(1, 2) == transform_and_reduce(square, lambda x, y: x+y, 1, 2))
print(cube_and_mul(1, 2) == transform_and_reduce(cube, lambda x, y: x*y, 1, 2))
print(quad_and_div(1, 2) == transform_and_reduce(quad, lambda x, y: x/y, 1, 2))


True
True
True

Operators to the rescure


In [9]:
import operator

print(square_and_add(1, 2) == transform_and_reduce(square, operator.add, 1, 2))
print(cube_and_mul(1, 2) == transform_and_reduce(cube, operator.mul, 1, 2))
print(quad_and_div(1, 2) == transform_and_reduce(quad, operator.truediv, 1, 2))


True
True
True

Lets do some maths

Number of transform functions = m

Number of reduce functions = n

Number of functions in the first workflow = m*n

Number of functions in the second workflow = m + n

Write small, re-useable function


In [10]:
print(square_and_add(1, 2) == transform_and_reduce(lambda x: x**2, lambda x, y: x+y, 1, 2))
print(cube_and_mul(1, 2) == transform_and_reduce(lambda x: x**3, lambda x, y: x*y, 1, 2))
print(quad_and_div(1, 2) == transform_and_reduce(lambda x: x**4, lambda x, y: x/y, 1, 2))


True
True
True

Function returns Function


In [11]:
from time import time
def timer(func):
    def inner(*args, **kwargs):
        t = time()
        func(*args, **kwargs)
        print("Time take = {time}".format(time = time() - t))
    return inner

def echo_func(input):
    print(input)
    
timed_echo = timer(echo_func)

timed_echo(1000000)


1000000
Time take = 0.0001728534698486328

Partial Functions


In [12]:
def logger(level, message):
    print("{level}: {message}".format(level = level, message = message))

def debug(message):
    return logger("debug", message)

def info(messgae):
    return logger("info", message)

debug("Error 404")


debug: Error 404

In [13]:
from functools import partial

debug = partial(logger, "debug")
info = partial(logger, "info")

debug("Error 404")


debug: Error 404

debug("Error 404") = partial(logger, "debug")("Error 404")


In [14]:
partial(logger, "debug")("Error 404")


debug: Error 404

Currying

f(a, b, c) => g(a)(b)(c)


In [15]:
def transform_and_add(func_transform, a, b):
    return func_transform(a) + func_transform(b)

def curry_transform_and_add(func_transform):
    def apply(a, b):
        return func_transform(a) + func_transform(b)
    
    return apply

In [16]:
print(transform_and_add(cube, 1, 2) == curry_transform_and_add(cube)(1, 2))


True

Currying gets you specialized functions from more general functions

Map, Reduce, Filter

An alternate view to iteration


In [17]:
input_list = [1, 2, 3, 4]
squared_list = map(lambda x: x**2, input_list)
print(type(squared_list))
print(next(squared_list))
print(next(squared_list))


<class 'map'>
1
4

In [18]:
from functools import reduce
sum_list = reduce(operator.add, input_list)
print(sum_list)


10

In [19]:
sum_squared_list = reduce(operator.add, 
                          map(lambda x: x**2, input_list))
print(sum_squared_list)


30

In [20]:
even_list = list(
                filter(lambda x: x%2==0, input_list))
sum_even_list = reduce(operator.add, even_list)
print(sum_even_list)


6

In [21]:
print(reduce(operator.add,
            (map(lambda x: x**2, 
                filter(lambda x: x%2==0, input_list)))))


20

Benefits

  • Functional
  • One-liner
  • Elemental operations

itertools — Functions creating iterators for efficient looping


In [22]:
from itertools import accumulate
acc = accumulate(input_list, operator.add)
print(input_list)
print(type(acc))
print(next(acc))
print(next(acc))
print(next(acc))


[1, 2, 3, 4]
<class 'itertools.accumulate'>
1
3
6

Recursion


In [23]:
def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n - 1)

Sadly, no tail recursion

Comprehension


In [24]:
print(input_list)
collection = list()
is_even = lambda x: x%2==0
for data in input_list:
    if(is_even(data)):
        collection.append(data)
    else:
        collection.append(data*2)
print(collection)


[1, 2, 3, 4]
[2, 2, 6, 4]

In [25]:
collection = [data if is_even(data) else data*2 
              for data in input_list]
print(collection)


[2, 2, 6, 4]

Generators


In [26]:
collection = (data if is_even(data) else data*2 
              for data in input_list)
print(collection)


<generator object <genexpr> at 0x7f7c5c1214c8>

Pipelines

Sequence of Operations


In [27]:
def pipeline_each(data, fns):
    return reduce(lambda a, x: map(x, a),
                  fns,
                  data)

In [28]:
import re

strings_to_clean = ["apple https://www.apple.com/",
                    "google https://www.google.com/",
                    "facebook https://www.facebook.com/"]

def format_string(input_string):
    return re.sub(r"http\S+", "", input_string).strip().title()

for _str in map(format_string, strings_to_clean):
    print(_str)


Apple
Google
Facebook

No Modularity


In [29]:
import re

def remove_url(input_string):
    return re.sub(r"http\S+", "", input_string).strip()

def title_case(input_string):
    return input_string.title()

def format_string(input_string):
    return title_case(remove_url(input_string))

for _str in map(format_string, strings_to_clean):
    print(_str)


Apple
Google
Facebook

f(g(h(i(...x)))

Modular but Ugly


In [30]:
import re

for _str in pipeline_each(strings_to_clean, [remove_url,
                                            title_case]):
    print(_str)


Apple
Google
Facebook

[f, g, h, i, ...]

Modular and Concise

Thank You

Shagun Sodhani

@shagunsodhani