34.1 First-Class Functions: Passing and Returning Functions
In functional programming, functions are treated as first-class citizens. This means they can be assigned to variables, stored in data structures, passed as arguments to other functions, and returned as values from other functions, just like any other object (e.g., integers, strings, or lists). This capability is the foundational bedrock upon which higher-order functions like map, filter, and reduce are built. Understanding first-class functions is crucial for writing concise, expressive, and powerful Python code.
Assigning Functions to Variables
A function, without the parentheses (), is a reference to a callable object. You can assign this reference to a new variable, creating an alias for the function. This doesn’t call the function; it merely creates another name for it.
def greet(name):
return f"Hello, {name}!"
# Assign the function 'greet' to the variable 'salutation'
salutation = greet # Note: no parentheses
# Now 'salutation' can be used exactly like 'greet'
print(salutation("Alice")) # Output: Hello, Alice!
print(salutation is greet) # Output: True (they are the same object)
This is powerful because it allows you to dynamically choose which function to use at runtime. For example, you could have a dictionary mapping string commands to their corresponding function handlers.
Passing Functions as Arguments
The most common application of first-class functions is passing them as arguments to other functions, thus creating higher-order functions. The receiving function can then invoke the passed-in function, often with data it controls. This is the exact mechanism behind map and filter.
def apply_operation(data, operation):
"""A higher-order function that applies a given operation to each element."""
return [operation(item) for item in data]
def square(x):
return x * x
def capitalize(s):
return s.upper()
numbers = [1, 2, 3, 4]
words = ['apple', 'banana', 'cherry']
# Pass the 'square' function as the 'operation' argument
squared_numbers = apply_operation(numbers, square)
print(squared_numbers) # Output: [1, 4, 9, 16]
# Pass the 'capitalize' function as the 'operation' argument
capitalized_words = apply_operation(words, capitalize)
print(capitalized_words) # Output: ['APPLE', 'BANANA', 'CHERRY']
This pattern, known as the strategy pattern, decouples the code that defines the operation (square, capitalize) from the code that executes it (apply_operation). The higher-order function becomes a generic tool, and its behavior is customized by the function you pass into it.
Returning Functions from Functions
Functions can also create and return other functions. This is often used to create function factories—functions that generate specialized functions based on given parameters.
def create_multiplier(factor):
"""Returns a new function that multiplies its input by the given factor."""
def multiplier(x):
return x * factor
return multiplier
# Create specialized functions
double = create_multiplier(2)
triple = create_multiplier(3)
print(double(5)) # Output: 10 (5 * 2)
print(triple(5)) # Output: 15 (5 * 3)
Here, create_multiplier is a closure. The inner function multiplier “closes over” the variable factor from the outer function’s scope. This value of factor is retained and remembered by the returned function even after create_multiplier has finished executing. This is an incredibly powerful technique for creating configurable behavior.
Lambda Functions: Anonymous First-Class Citizens
The lambda keyword allows you to create small, anonymous functions without a formal def statement. They are expressions, not statements, which means they can be used inline wherever a function reference is expected. This is extremely common with map, filter, and sorted.
numbers = [1, 2, 3, 4, 5, 6]
# Using a lambda with filter instead of a named function
even_numbers = filter(lambda x: x % 2 == 0, numbers)
print(list(even_numbers)) # Output: [2, 4, 6]
# Using a lambda with sorted to customize sorting
pairs = [(1, 'z'), (3, 'a'), (2, 'y')]
sorted_by_second = sorted(pairs, key=lambda pair: pair[1])
print(sorted_by_second) # Output: [(3, 'a'), (2, 'y'), (1, 'z')]
Common Pitfalls and Best Practices
Accidental Invocation: The most common mistake is adding parentheses
()when you intend to pass a function reference.my_funcis the function object;my_func()calls the function and passes its return value.# Wrong: passes the result of greet(), which is a string, not the function result = apply_operation(names, greet("prelude")) # TypeError # Correct: passes the function itself result = apply_operation(names, greet)Lambda Overuse: While convenient, lambdas should be kept simple. If your logic is complex, requires loops, or is more than a single expression, define a named function with
deffor better readability and debuggability. PEP 8 explicitly advises against assigning lambmas to variables; use adefinstead.# Discouraged by PEP 8 square = lambda x: x ** 2 # Encouraged def square(x): return x ** 2Closure Variable Binding: A subtle pitfall occurs when creating lambdas or inner functions inside a loop. The variable from the loop is bound by name, not by value. This often leads to all returned functions using the final value of the loop variable.
# A common bug: all functions will use i=4 multipliers = [lambda x: x * i for i in range(5)] print(multipliers[1](10)) # Unexpected output: 40, not 10 # The fix: capture the value at the time of creation using a default argument multipliers = [lambda x, i=i: x * i for i in range(5)] # i=i captures the current value of i print(multipliers[1](10)) # Correct output: 10