Notes on Functional Programming in Python

By: Cam Wohlfeil
Published: 2019-03-07 1200 EST
Category: Programming
Tags: python, functional

Lambda Expressions

Lambda expressions are simply small anonymous functions. I could go in to more detail, but I'd be hard pressed to beat Dan Bader's explanation linked at the end. Basically, you want to use them when you need a single expression that won't be needed elsewhere in your code.

# Bad example, by assigning it to a variable we've named our lambda, defeating the purpose
add = lambda x, y: x + y
add(5, 3)
# 8

# Good example
items = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, items))

First Class Functions

Python has first class functions, meaning they're treated like any other variable (i.e. a function can be passed as an argument to other functions, can be returned by another function and can be assigned as a value to a variable).

Recursion

Recursion is when a function calls itself until the base case is met. Think of it like a loop, except the next iteration takes place within the previous iteration, until they all return.

# Basic examples
def mysum(nums):
    if nums == []:
        #Base case, no further recursion needed
        return 0
    else:
        # Else call self again until base case is met, minus first (current) element
        return nums[0] + mysum(nums[1:])

print(mysum([1,2,3]))

def add1_all(nums):
    if nums == []:
        #Base case, no further recursion needed
        return []
    else:
        # Else call self again until base case is met, minus first (current) element
        return [nums[0] + 1] + add1_all(nums[1:])

print(add1_all([1,2,3]))
# 6
# [2, 3, 4]

# More realistic
class Person(object):
    def __init__(self, name):
        self.name = name

class Group(object):
    """Grounps can contain Persons or more Groups."""
    def __init__(self, members, subgroups):
        self.members = members
        self.subgroups = subgroups

def get_all_members(group):
    sub_members = []
    for subgroup in group.subgroups:
        # Base case is group with no subgroups is passed
        sub_members.extend(get_all_members(subgroup))
    return group.members + sub_members

group = Group(['Sam', 'Jessie'], [Group(['Reese', 'Taylor'], [])])
print(get_all_members(group))
# ['Sam', 'Jessie', 'Reese', 'Taylor']

Higher-Order Functions

Higher-order functions do at least one of the following:

The why is more complicated, but this is so we can handle things that are unknown until execution time. See the HackerNoon article referenced at the end for more information.

# Abstract our recursive algo with classes...
class Mapper(object):
    def compute(self, element):
        raise NotImplementedError("Subclass and override me!")

    def map(self, l):
        if l == []:
            return l
        return [self.compute(l[0])] + self.map(l[1:])

# Or as a function
def mymap(f, l):
        if l == []:
            return l
        return [f(l[0])] + mymap(f, l[1:])

print(mymap(lambda n: n+2, [1,2,3]))

# Decorators
def trace(name, x, f):
    print("<{}({})>".format(name, x), end='')
    result = f(x)
    print("</{}({}): {}>".format(name, x, result), end='')
    return result

def trace_detector(f):
    return lambda x: trace(f.__name__, x, f)

@trace_detector
def doWork(x):
    return subWork(x) + otherWork(x)

@trace_detector
def subWork(x):
    return x * 2

@trace_detector
def otherWork(x):
    return x - 5

doWork(50)
# [3, 4, 5]
# <doWork(50)><subWork(50)></subWork(50): 100><otherWork(50)></otherWork(50): 45> </doWork(50): 145>
# 145

Modifying Data Structures

In functional programming, data structures are immutable, i.e. never modified, only replaced. This is so there's never an issue with shared state, but can kill cache performance.

# Simple immutability, pass a data struct and return the new one.
def add_item(l):
    return l + ['milk']

print(add_item(['cookies', 'jelly']))

# With dicts, we can use the copy method
a = {'foo': 'bar'}
b = {'baz': 'quux'}
c = a.copy()
c.update(b)
print(c, a, b)

# Similar with objects
import copy
class Foo(object):
    def __init__(self):
        self.x = 1

    def set_x(self, v):
        self.x = v

o = Foo()
new_o = copy.copy(o)
new_o.set_x(2)
print(o.x, new_o.x)
# ['cookies', 'jelly', 'milk']
# {'foo': 'bar', 'baz': 'quux'} {'foo': 'bar'} {'baz': 'quux'}
# 1 2

Currying

Currying is when you break down a function that takes multiple arguments into a series of functions that take part of the arguments.

def add(a):
    return lambda b: a + b

print(add(1)(2))

def square(a):
    # An adapter pattern?
    return lambda: a**2

print(square(2))
3
<function square.<locals>.<lambda> at 0x0000025C49B84D90>

Closure

A closure occurs when a function has access to a local variable from an enclosing scope that has finished its execution. You are essentially using local scope to wrap a variable and provide "shared" state safely.

def make_printer(msg):
    # Closure
    def printer():
        print(msg)
    return printer

printer = make_printer('Foo!')
printer()

def make_printer(msg):
    # Not a closure
    def printer(msg=msg):
        print(msg)
    return printer

printer = make_printer("Foo!")
printer()
# Foo!
# Foo!

Map, Filter and Reduce

These three functions are critical to functional programming.

Map

Map applies a function to all the items in an iterable input. Most of the times we want to pass all the elements to a function one-by-one and then collect the output. For instance:

items = [1, 2, 3, 4, 5]
squared = []
for i in items:
    squared.append(i**2)

# Map allows us to implement this in a simpler way
items = [1, 2, 3, 4, 5]
# Most of the time lambdas are used with map
squared = list(map(lambda x: x**2, items))

# Instead of a list of inputs we can have a list of functions
def multiply(x):
    return (x*x)
def add(x):
    return (x+x)

funcs = [multiply, add]
for i in range(5):
    value = list(map(lambda x: x(i), funcs))
    print(value)

# [0, 0]
# [1, 2]
# [4, 4]
# [9, 6]
# [16, 8]

Filter

Filter creates a list of elements for which a function returns true.

number_list = range(-5, 5)
less_than_zero = list(filter(lambda x: x < 0, number_list))
print(less_than_zero)
# [-5, -4, -3, -2, -1]
# Filter resembles a for loop but it is a builtin function and faster

If map & filter seem ugly or confusing then you should read about list/dict/tuple comprehensions.

Reduce

Reduce performs some computation on an iterable and returns the result. It applies a rolling computation to sequential pairs of values in a list. For example, if you wanted to compute the product of a list of integers.

# You might go about doing this using a basic for loop
product = 1
list = [1, 2, 3, 4]
for num in list:
    product *= num
# product = 24

# With reduce
from functools import reduce
product = reduce((lambda x, y: x * y), [1, 2, 3, 4])
# 24

References