Functions Returning Functions
In Python, higher-order functions can not only accept other functions as arguments but also return functions as results. Consider a typical summation function:
def calc_sum(*args):
total = 0
for num in args:
total += num
return total
However, if the sum doesn't need to be computed immediately—but rather deferred until later—you can return a function instead of the result:
def lazy_sum(*args):
def compute():
total = 0
for num in args:
total += num
return total
return compute
Calling lazy_sum() returns a function object. The actual computation occurs only when this returned function is invoked. The inner function compute retains access to the args from its enclosing scope—even after lazy_sum has finished executing. This behavior defines a closure.
Each invocation of lazy_sum() creates a new closure with its own captured variables. Thus, separate calls produce independent functions whose internal states do not interfere with one another.
Understanding Closures and Common Pitfalls
A key characteristic of closures is that the returned function references variables from the outer function’s scope. However, these variables are not copied—they are referenced by name. This leads to subtle issues when those variables chenge over time.
Consider this example:
def count():
funcs = []
for i in range(1, 4):
def square():
return i * i
funcs.append(square)
return funcs
f1, f2, f3 = count()
One might expect f1(), f2(), and f3() to return 1, 4, and 9 respectively. Instead, all three return 9. Why? Because each square function refers to the same variable i, which—by the time any of them execute—has already reached its final value of 3 after the loop completes.
To avoid this, bind the current value of the loop variable at the time the closure is created. One reliable approach is to pass the variable as a argument to an intermediate function:
def count():
def make_square(j):
def square():
return j * j
return square
funcs = []
for i in range(1, 4):
funcs.append(make_square(i))
return funcs
Now, each closure captures its own unique value of j, yielding the expected results: 1, 4, and 9. While more verbose, this pattern ensures correctness. Alternatively, lambda expressions can be used for brevity, though readability may suffer.