Functional Python with Map and Filter

Functional Python using the built-in functions map and filter. Includes an interactive coding challenge.

Functional Programming

Functional programming is a programming paradigm which decomposes programs into a set of functions. The functions should take an input and produce an output that is deterministic. In other words, a function should always return the same output given a certain input. Furthermore, in functional programming, functions should not maintain internal state thus eliminating potential side effects.

Python is not a pure functional programming language, like Haskell or others, but there are functional features in Python for applying and composing functions. The built-in functions map and filter are examples of functional programming within Python.

While list comprehensions are usually the preferred syntax for concisely creating lists, map and filter are also applicable in some scenarios for an elegant solution.

Map

map accepts a function and one or more iterables as inputs. The result of calling map is a map object iterator. Each item from the iterable or sequence is passed through the supplied function per iteration. The snippet below demonstrates the basic usage.

func = lambda x: x**2  # anonymous function to operate on item values
iterable = range(1, 11)  # sequence of integers from 1 to 10
squares = map(func, iterable)  # generates a map object iterator

# Calling next processes and removes the next item from the iterator
print(next(squares))  # Output: 1
print(next(squares))  # output 4

# List the remaining 8 items in the iterator
print(list(squares))  # Output: [4, 9, 16, 25, 36, 49, 64, 81, 100]
print(list(squares))  # Output: []

If multiple iterables are passed to map, such as two lists, the applied function should accept a matching number of arguments.

func = lambda x, y: x+y  # anonymous function to operate on item values
iterable = range(1, 11)  # sequence of integers from 1 to 10
neg_iterable = range(-1, -11, -1)  # sequence of integers from -1 to -10
squares = map(func, iterable, neg_iterable)  # generates a map object iterator

print(list(squares))  # Output: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

Filter

Similar to map, filter accepts a function as an argument but only up to one iterable. filter also returns an iterator (filter object). Only the items from the iterable which pass the conditional logic are included in the final output.

func = lambda x: x % 2 == 0  # anonymous function to conditionally filter items
iterable = range(1, 11)  # sequence of integers from 1 to 11
squares = filter(func, iterable)  # generates a filter object

print(list(squares))  # Output: [2, 4, 6, 8, 10]

Map and Filter

The two built-in functions can also be combined together creating a functional pairing.

map_func = lambda x: x**2  # anonymous function to operate on item values
filter_func = lambda x: x % 2 == 0  # anonymous function to conditionally filter
iterable = range(1, 11)  # sequence of integers from 1 to 11

# Apply the filter result as an iterable input to map
squares = map(map_func, filter(filter_func, iterable))

print(list(squares))  # Output: [4, 16, 36, 64, 100]

Complex Scenario

For some complex scenarios, map and filter offer more graceful and readable solutions than normal for loops or list comprehensions.

The objective in the example problem below is to create average student grades per class (not to be confused with a Python class object). The sample data contains a list of three classes each containing three exam grades from three students (3x3x3 matrix).

For Loops

The first implementation to review is regular for loops. The result is code bloat and logic that is mentally taxing to follow.

# Helper function to calculate an average given a list of numbers
from statistics import mean

# Sample data of 3 classes with 3 exam grades from 3 students
grades = [
    [[65, 87, 94], [60, None, 58], [93, 83, 76]],
    [[79, 85, 82], [72, 70, 77], [88, 86, 90]],
    [[92, 88, 96], [84, 88, 86], [95, 96, 97]]
]

# Compute student grade averages per class using for loops
averages = []  # Stateful list of averages
for class_grades in grades:
    class_avgs = []  # Stateful list of student averages per class
    for student_grades in class_grades:
        exam_avgs = []  # Stateful list of exam averages per student
        for exam_grade in student_grades:
            if exam_grade is not None:
                exam_avgs.append(exam_grade)

        class_avgs.append(mean(exam_avgs))
    averages.append(class_avgs)

print(averages)

# Output: [[82, 59, 84], [82, 73, 88], [92, 86, 96]]

List Comprehensions

The second implementation, for comparison, uses list comprehensions. Frankly, it's challenging to format coherently and more difficult to reason about.

# Helper function to calculate an average given a list of numbers
from statistics import mean

# Sample data of 3 classes with 3 exam grades from 3 students
grades = [
    [[65, 87, 94], [60, None, 58], [93, 83, 76]],
    [[79, 85, 82], [72, 70, 77], [88, 86, 90]],
    [[92, 88, 96], [84, 88, 86], [95, 96, 97]]
]

# Compute student grade averages per class using a list comprehension
averages = [[mean([exam_grade for exam_grade in student_grades
                   if exam_grade is not None])
			for student_grades in class_grades]
			for class_grades in grades]

print(averages)

# Output: [[82, 59, 84], [82, 73, 88], [92, 86, 96]]

Map and Filter

Lastly, map and filter create a polished solution in conjunction together. The logic is explicitly described with named functions rather than anonymous, lambda functions like the initial examples at the top.

# Helper function to calculate an average given a list of numbers
from statistics import mean

# Sample data of 3 classes with 3 exam grades from 3 students
grades = [
    [[65, 87, 94], [60, None, 58], [93, 83, 76]],
    [[79, 85, 82], [72, 70, 77], [88, 86, 90]],
    [[92, 88, 96], [84, 88, 86], [95, 96, 97]]
]

# Conditional filter logic exclude missed exam grades
def filter_exam_grades(exam_grade):
    return exam_grade is not None

# Calculate average per list of student exam grades
def calculate_student_avgs(student_grades):
    exam_grades = filter(filter_exam_grades, student_grades)
    return mean(exam_grades)

# Calculate student averages per list of class grades
def calculate_class_avgs(class_grades):
    return list(map(calculate_student_avgs, class_grades))

# Calculate student grade averages per class
averages = map(calculate_class_avgs, grades)

print(list(averages))

# Output: [[82, 59, 84], [82, 73, 88], [92, 86, 96]]

Coding Challenge

For an interactive challenge, can you fix the code below? The exam grades use a different data format per class. Running the program as-is throws the error exception TypeError: can't convert type 'str' to numerator/denominator. We need to resolve the error and match the same output from above [[82, 59, 84], [82, 73, 88], [92, 86, 96]].

Hint: map or list comprehension will be helpful to normalize all exam grades as integers.

Loading...

Summary

The first two implementations contain deeply nested loops that are difficult to read and follow. On the other hand, the map and filter solution is well-defined. Moreover, the functional programming implementation contains bite size units of functionality which are independently testable with a deterministic output.

Regular for loops and especially list comprehensions cover a wide range of scenarios in Python for iterating over data sequences. Occasionally, you may want to pull from your functional programming toolbox with map and filter for an elegant alternative.

Published