Skip to content

Higher-Order Functions & Closures

Overview

Higher-order functions work with other functions:

  • Accept functions as arguments: Customize behavior
  • Return functions: Create function factories
  • Closures: Inner functions capture outer scope
  • Partial application: "Lock in" some arguments
  • Currying: Convert multi-argument functions to chained single-argument functions

Higher-Order Functions

Functions as Arguments

# Higher-order function
def apply_twice(func, x):
 """Apply function twice."""
 return func(func(x))

# Use with different functions
square = lambda x: x ** 2
add_one = lambda x: x + 1

print(apply_twice(square, 3)) # (3²)² = 81
print(apply_twice(add_one, 3)) # (3+1)+1 = 5

# Built-in higher-order functions
numbers = [1, 2, 3, 4, 5]

# map
squared = list(map(lambda x: x ** 2, numbers))
print(squared) # [1, 4, 9, 16, 25]

# filter
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens) # [2, 4]

# reduce
from functools import reduce
total = reduce(lambda acc, x: acc + x, numbers, 0)
print(total) # 15

Practical Higher-Order Functions

def process_batch(data, *transforms):
 """Apply multiple transformations to data."""
 result = data
 for transform in transforms:
 result = transform(result)
 return result

# Use it
data = [1, 2, 3, 4, 5]

def double(x):
 return [i * 2 for i in x]

def filter_even(x):
 return [i for i in x if i % 2 == 0]

def sum_all(x):
 return sum(x)

result = process_batch(data, double, filter_even, sum_all)
print(result) # [4] doubled → [2,4,6,8,10], filter → [4,8], sum → 12

-

Functions Returning Functions

Function Factories

# Factory that creates functions
def make_adder(n):
 """Create a function that adds n to its argument."""
 def adder(x):
 return x + n
 return adder

# Create specialized functions
add_5 = make_adder(5)
add_10 = make_adder(10)

print(add_5(3)) # 8
print(add_10(3)) # 13

# Factory for transformations
def make_scaler(factor):
 """Create scaling function."""
 return lambda x: x * factor

scale_2x = make_scaler(2)
scale_10x = make_scaler(10)

print(scale_2x(5)) # 10
print(scale_10x(5)) # 50

# Factory for composition
def make_pipeline(*operations):
 """Create pipeline of operations."""
 def pipeline(x):
 result = x
 for op in operations:
 result = op(result)
 return result
 return pipeline

normalize = lambda x: (x - x.mean()) / x.std()
scale = lambda x: x * 255
preprocess = make_pipeline(normalize, scale)

Closures

Capturing Outer Scope

# Closure
def outer(x):
 # This variable is captured by inner
 captured_value = x * 10

 def inner(y):
 # inner "closes over" captured_value
 return y + captured_value

 return inner

# Create closures with different captured values
add_10 = outer(1) # Captures 1*10 = 10
add_50 = outer(5) # Captures 5*10 = 50

print(add_10(3)) # 3 + 10 = 13
print(add_50(3)) # 3 + 50 = 53

# Practical closure
def make_counter():
 """Create a counter with internal state."""
 count = 0

 def increment():
 nonlocal count # Modify outer variable
 count += 1
 return count

 def get():
 return count

 return increment, get

inc1, get1 = make_counter()
inc2, get2 = make_counter()

print(inc1()) # 1
print(inc1()) # 2
print(inc2()) # 1 (separate counter!)
print(get1()) # 2
print(get2()) # 1

Closure with Configuration

# Closure captures configuration
def create_classifier(threshold):
 """Create classifier with specific threshold."""
 def classify(value):
 if value > threshold:
 return "high"
 else:
 return "low"
 return classify

# Different classifiers
high_threshold = create_classifier(0.8)
low_threshold = create_classifier(0.3)

print(high_threshold(0.9)) # "high"
print(high_threshold(0.5)) # "low"
print(low_threshold(0.5)) # "high"

Partial Application

Using functools.partial

from functools import partial

def power(base, exponent):
 """Compute base^exponent."""
 return base ** exponent

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

print(square(5)) # 25
print(cube(5)) # 125

# More practical example
def format_string(template, *args, **kwargs):
 return template.format(*args, **kwargs)

greet = partial(format_string, "Hello, {}!")
print(greet("Alice")) # "Hello, Alice!"
print(greet("Bob")) # "Hello, Bob!"

# Practical ML use
def train_model(model, optimizer, data, learning_rate, epochs):
 for epoch in range(epochs):
 # training code
 pass

# Create trainer with fixed LR and epochs
train_with_adam = partial(train_model, optimizer='adam', learning_rate=0.001, epochs=100)
# Now only need to provide model and data

Currying

Converting Multi-Arg to Single-Arg Functions

# Curried function
def curried_add(a):
 def with_b(b):
 def with_c(c):
 return a + b + c
 return with_c
 return with_b

# Use it
result = curried_add(1)(2)(3) # 6

# Or use intermediate steps
add_one = curried_add(1)
add_one_and_two = add_one(2)
result = add_one_and_two(3) # 6

# Currying utility
def curry(func):
 """Convert function to curried form."""
 from functools import wraps
 @wraps(func)
 def curried(*args, **kwargs):
 if len(args) + len(kwargs) >= func.__code__.co_argcount:
 return func(*args, **kwargs)
 return partial(curried, *args, **kwargs)
 return curried

@curry
def multiply(a, b, c):
 return a * b * c

print(multiply(2)(3)(4)) # 24
print(multiply(2, 3)(4)) # 24
print(multiply(2, 3, 4)) # 24

Decorators as Higher-Order Functions

Function Wrapping

from functools import wraps

# Decorator is a higher-order function
def timing_decorator(func):
 """Measure function execution time."""
 @wraps(func)
 def wrapper(*args, **kwargs):
 import time
 start = time.time()
 result = func(*args, **kwargs)
 elapsed = time.time() - start
 print(f"{func.__name__} took {elapsed:.4f}s")
 return result
 return wrapper

@timing_decorator
def slow_function(n):
 total = 0
 for i in range(n):
 total += i ** 0.5
 return total

slow_function(1000000)
# Prints

Parametrized Decorators

# Decorator factory returns decorator
def repeat(times):
 """Decorator that repeats function execution."""
 def decorator(func):
 @wraps(func)
 def wrapper(*args, **kwargs):
 results = []
 for _ in range(times):
 results.append(func(*args, **kwargs))
 return results
 return wrapper
 return decorator

@repeat(3)
def greet(name):
 return f"Hello, {name}!"

print(greet("Alice"))
# Output

Real-World ML Examples

Higher-Order Loss Function

import torch

def create_weighted_loss(weights):
 """Factory for weighted loss functions."""
 def loss_fn(predictions, targets):
 errors = (predictions - targets) ** 2
 weighted = errors * weights
 return weighted.mean()
 return loss_fn

# Different loss functions for different data
loss_balanced = create_weighted_loss(torch.ones(10))
loss_biased = create_weighted_loss(torch.tensor([1.0] * 5 + [0.5] * 5]))

Higher-Order Data Transform

def create_augmentation_pipeline(*augmentations):
 """Create data augmentation pipeline."""
 def augment(batch):
 result = batch
 for aug in augmentations:
 result = aug(result)
 return result
 return augment

# Create pipelines
rotate = lambda x: x # placeholder
flip = lambda x: x # placeholder
scale = lambda x: x # placeholder

train_augment = create_augmentation_pipeline(rotate, flip, scale)
test_augment = create_augmentation_pipeline() # No augmentation

Summary: Higher-Order Function Patterns

Pattern Use Case Example
map/filter Transform collections map(square, numbers)
Function factory Create specialized functions make_adder(5)
Closure Capture state/config create_classifier(0.8)
Partial application Fix arguments partial(func, x=10)
Currying Single-argument functions curry(func)(a)(b)
Decorators Wrap functions @decorator

-

  • 01 Functional Programming Fundamentals - Pure functions
  • [03 Function Composition & Piping](/05-py3/03-functional-programming/(03-function-composition-piping/) - Composing higher-order functions
  • 02 Decorators - Decorator patterns
  • [03 Iterator & Generator Protocol](/05-py3/01-fundamentals/(03-iterator-generator-protocol/) - Iterator functions