Higher-Order Functions in Python
Higher-order functions either take other functions as arguments or return functions. Python treats functions as first-class objects, making higher-order functions straightforward to use.
Contents
Functions as arguments
Functions can be passed as arguments to other functions, enabling flexible and reusable code.
def apply_operation(x, y, operation):
return operation(x, y)
def add(a, b):
return a + b
def multiply(a, b):
return a * b
print(apply_operation(5, 3, add))
>>> 8
print(apply_operation(5, 3, multiply))
>>> 15
You can pass lambda functions directly.
def apply_operation(x, y, operation):
return operation(x, y)
result = apply_operation(10, 5, lambda a, b: a - b)
print(result)
>>> 5
Built-in functions like map(), filter(), and sorted() are higher-order functions.
numbers = [1, 2, 3, 4, 5]
# map() takes a function as first argument
squared = list(map(lambda x: x ** 2, numbers))
print(squared)
>>> [1, 4, 9, 16, 25]
# filter() takes a function as first argument
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens)
>>> [2, 4]
# sorted() takes a function as key argument
words = ["apple", "banana", "cherry"]
sorted_words = sorted(words, key=len)
print(sorted_words)
>>> ['apple', 'cherry', 'banana']
Functions as return values
Functions can return other functions, enabling powerful patterns like function factories.
def create_multiplier(n):
def multiplier(x):
return x * n
return multiplier
double = create_multiplier(2)
triple = create_multiplier(3)
print(double(5))
>>> 10
print(triple(5))
>>> 15
The returned function "remembers" the values from the enclosing scope.
def create_adder(n):
def adder(x):
return x + n
return adder
add_five = create_adder(5)
add_ten = create_adder(10)
print(add_five(3))
>>> 8
print(add_ten(3))
>>> 13
Function factories
Function factories create and return specialised functions based on parameters.
def create_validator(min_value, max_value):
def validate(value):
if min_value <= value <= max_value:
return True
return False
return validate
age_validator = create_validator(18, 65)
print(age_validator(25))
>>> True
print(age_validator(70))
>>> False
You can create multiple validators with different criteria.
def create_comparator(operator):
if operator == "greater":
return lambda x, y: x > y
elif operator == "less":
return lambda x, y: x < y
elif operator == "equal":
return lambda x, y: x == y
else:
return lambda x, y: False
greater_than = create_comparator("greater")
print(greater_than(5, 3))
>>> True
print(greater_than(3, 5))
>>> False
Decorators
Decorators are a common use of higher-order functions. They modify or extend the behaviour of other functions.
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with {args}, {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
print(add(3, 4))
>>> Calling add with (3, 4), {}
>>> add returned 7
>>> 7
Decorators can accept arguments themselves.
def repeat(times):
def decorator(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"))
>>> ['Hello, Alice!', 'Hello, Alice!', 'Hello, Alice!']
You can use multiple decorators on a single function.
def uppercase(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
def add_exclamation(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result + "!"
return wrapper
@add_exclamation
@uppercase
def greet(name):
return f"hello, {name}"
print(greet("alice"))
>>> HELLO, ALICE!
Closures
Closures occur when a nested function references variables from its enclosing scope. This is fundamental to higher-order functions.
def outer_function(x):
def inner_function(y):
return x + y
return inner_function
add_five = outer_function(5)
print(add_five(3))
>>> 8
The closure "captures" the variable from the outer scope.
def create_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
counter1 = create_counter()
counter2 = create_counter()
print(counter1())
>>> 1
print(counter1())
>>> 2
print(counter2())
>>> 1
Each closure maintains its own state.
Common higher-order patterns
Higher-order functions enable many useful patterns like function composition and partial application.
def compose(f, g):
def composed(x):
return f(g(x))
return composed
def add_one(x):
return x + 1
def multiply_two(x):
return x * 2
add_then_multiply = compose(multiply_two, add_one)
print(add_then_multiply(5))
>>> 12
You can create a timing decorator for performance measurement.
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper
@timer
def slow_function():
time.sleep(0.1)
return "Done"
slow_function()
>>> slow_function took 0.1001 seconds
Higher-order functions work well with error handling.
def handle_errors(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Error in {func.__name__}: {e}")
return None
return wrapper
@handle_errors
def divide(a, b):
return a / b
print(divide(10, 2))
>>> 5.0
print(divide(10, 0))
>>> Error in divide: division by zero
>>> None