Closures & Functional Programming

Difficulty: Advanced

A closure is a function that remembers the values from its enclosing scope even after that scope has finished executing. In Python, when a nested function references variables from its parent function, and the parent function returns the nested function, the returned function 'closes over' those variables, keeping them alive. This is the foundation of decorators and many functional patterns.

Closures are created when three conditions are met: there is a nested function, the nested function references a variable from the enclosing function, and the enclosing function returns the nested function. The captured variables are stored in the function's __closure__ attribute as cell objects. Understanding closures is key to understanding how decorators, callbacks, and factory functions work in Python.

The functools module provides essential tools for functional programming. functools.partial creates a new function with some arguments pre-filled, which is useful for adapting functions to different interfaces. functools.reduce applies a two-argument function cumulatively to a sequence, reducing it to a single value -- for example, reduce(operator.add, [1,2,3,4]) computes ((1+2)+3)+4 = 10.

The operator module provides function equivalents of Python's operators: operator.add for +, operator.mul for *, operator.itemgetter for indexing, and operator.attrgetter for attribute access. These are faster than equivalent lambda functions and more readable when passed to higher-order functions like map(), filter(), sorted(), and reduce().

Functional programming patterns in Python include using map() and filter() for transformations, reduce() for aggregation, and combining these with lambda or operator functions for concise data processing pipelines. While Python is not a purely functional language, these tools are powerful when used appropriately alongside imperative code.

Code examples

Basic Closure

def make_multiplier(factor):
    def multiply(n):
        return n * factor
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5))
print(triple(5))
print(double(10))

make_multiplier returns multiply, which 'closes over' factor. Each call creates a separate closure with its own factor value.

Using functools.partial

from functools import partial

def power(base, exponent):
    return base  exponent

square = partial(power, exponent=2)
cube = partial(power, exponent=3)

print(square(5))
print(cube(3))
print(square(10))

partial freezes the exponent argument, creating specialized versions of power. This is cleaner than using lambda x: power(x, 2).

Using functools.reduce

from functools import reduce

numbers = [1, 2, 3, 4, 5]

total = reduce(lambda a, b: a + b, numbers)
print(total)

product = reduce(lambda a, b: a * b, numbers)
print(product)

max_val = reduce(lambda a, b: a if a > b else b, numbers)
print(max_val)

reduce applies the function cumulatively: total computes ((((1+2)+3)+4)+5)=15. It reduces a sequence to a single value left to right.

Operator Module with sorted and map

from operator import itemgetter, mul
from functools import reduce

students = [
    {"name": "Alice", "grade": 88},
    {"name": "Bob", "grade": 95},
    {"name": "Charlie", "grade": 82},
]

sorted_students = sorted(students, key=itemgetter("grade"), reverse=True)
for s in sorted_students:
    print(f"{s['name']}: {s['grade']}")

factorial_5 = reduce(mul, range(1, 6))
print(f"5! = {factorial_5}")

itemgetter('grade') creates a callable that extracts the 'grade' key -- cleaner and faster than a lambda. operator.mul replaces lambda a,b: a*b for computing factorial.

Key points

Concepts covered

closures, functools.partial, functools.reduce, operator module, functional patterns