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:
- Takes one or more functions as arguments
- Returns a function as its result
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
- Python Docs: Functional Programming HOWTO
- Powerful Python: Python Functions Aren't What You Think
- Real Python: Inner Functions, What Are They Good For
- Real Python: Thinking Recursively in Python
- Dan Bader: First Class Function in Python
- Dan Bader: Lambda Functions in Python
- Hacker Noon: Higher Order Functions
- Toolz
- Dan Bader: Functional Linked Lists in Python