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: takes function as argument
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: Apply function to each element
squared = list(map(lambda x: x ** 2, numbers))
print(squared) # [1, 4, 9, 16, 25]
# filter: Keep elements where function returns True
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens) # [2, 4]
# reduce: Accumulate with function
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: inner function captures outer variables
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: counter
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: "lock in" base argument
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: takes one argument at a time
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: slow_function took X.XXXXs
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: ['Hello, Alice!', 'Hello, Alice!', 'Hello, Alice!']
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 |
Related Topics¶
- 01 Functional Programming Fundamentals - Pure functions
- 03 Function Composition & Piping - Composing higher-order functions
- 02 Decorators - Decorator patterns
- 03 Iterator & Generator Protocol - Iterator functions