Closures: Functions with a 'Memory'
We are now at the final article in our series on iterators, generators, and decorators. We will explore closures, a concept that is fundamental to how decorators and other advanced patterns work in Python.
A closure is a function object that "remembers" values in the enclosing scope even if they are not present in memory. It's a way to give a function a persistent "memory."
π Prerequisitesβ
You should have a strong understanding of nested functions and Python's variable scope rules (LEGB).
π― Article Outline: What You'll Masterβ
In this article, you will learn:
- β Nested Functions: A quick recap of defining functions inside other functions.
- β What a Closure Is: Understand how an inner function can "close over" and remember the variables from its parent function's scope.
- β
The
nonlocalKeyword: How to modify a variable from the enclosing scope within the inner function. - β A Practical Example: See how closures can be used to create "function factories" and stateful functions.
π§ Section 1: The Foundation - Nested Functionsβ
In Python, you can define a function inside another function.
def outer_function(text):
# The 'text' variable is local to outer_function
def inner_function():
# The inner function can access 'text' from its enclosing scope
print(text)
# The outer function calls the inner function
inner_function()
outer_function("Hello, world!") # Output: Hello, world!
This works because of the LEGB scope rule. inner_function doesn't have a text variable in its local scope, so it looks to its enclosing scope (the scope of outer_function) and finds it there.
π» Section 2: Creating a Closureβ
A closure is created when the nested function is returned by the outer function. The returned inner function remembers the variables from the scope where it was created.
Let's modify our previous example. Instead of calling inner_function, we'll return it.
# closure_example.py
def outer_function(text):
"""This is the outer function."""
def inner_function():
"""
This is the nested function. It 'closes over' the 'text' variable.
"""
print(text)
# Return the inner function itself, without calling it
return inner_function
# Call the outer function. It returns the inner_function.
my_closure = outer_function("Hello, closure!")
# 'my_closure' is now a closure. The outer_function has finished running,
# but the 'my_closure' function still remembers the 'text' variable.
print(type(my_closure))
my_closure() # Execute the inner function
Output:
<class 'function'>
Hello, closure!
This is the essence of a closure. Even though outer_function has completed and its local scope should be gone, the my_closure object maintains a link to the text variable from that scope.
π οΈ Section 3: Stateful Functions and the nonlocal Keywordβ
Closures are excellent for creating functions that need to maintain a state between calls, without using global variables or a class.
Let's create a function factory that produces "counter" functions.
# stateful_closure.py
def make_counter():
"""A factory for creating counter functions."""
count = 0 # This variable is in the enclosing scope
def counter():
"""The closure that increments and returns the count."""
# We need to tell Python that we want to modify the 'count'
# from the enclosing scope, not create a new local one.
nonlocal count
count += 1
return count
return counter
# Create two independent counters
counter_A = make_counter()
counter_B = make_counter()
print("--- Counter A ---")
print(counter_A())
print(counter_A())
print("\n--- Counter B ---")
print(counter_B())
print("\n--- Counter A again ---")
print(counter_A())
Output:
--- Counter A ---
1
2
--- Counter B ---
1
--- Counter A again ---
3
The nonlocal Keyword:
The nonlocal statement is crucial here. If we had just written count += 1, Python would have assumed we were trying to create a new local variable named count inside the counter function, which would lead to an UnboundLocalError. nonlocal count tells Python: "The count variable I'm about to modify is not mine; it belongs to the enclosing function."
Each time we call make_counter(), a new count variable is created in a new scope, and a new counter closure is created that "remember" that specific count. This is why counter_A and counter_B have their own independent states.
β¨ Conclusion & Key Takeawaysβ
Closures are a powerful feature that enables many advanced patterns in Python, most notably decorators. They provide a clean way to create functions with persistent state and to build function factories.
Let's summarize the key takeaways:
- A Closure is a function that remembers its enclosing scope. It's created when a nested function that references variables from its parent is returned by the parent function.
- They maintain state without the need for classes or global variables.
- Use the
nonlocalkeyword to modify a variable in the enclosing scope from within the inner function. - Closures are the building blocks for decorators. The "wrapper" function in a decorator is a closure.
Challenge Yourself:
Create a function factory called make_multiplier(n). This function should take a number n and return a new function (a closure). The returned function should take a single argument x and return the result of n * x.
times_3 = make_multiplier(3)
times_5 = make_multiplier(5)
print(times_3(9)) # Expected output: 27
print(times_5(3)) # Expected output: 15
β‘οΈ Next Stepsβ
Congratulations on completing this entire chapter on advanced Python concepts! You've explored iterators, generators, decorators, context managers, and closuresβall powerful tools for writing clean, efficient, and expressive code.
In our next chapter, we will begin our journey into the world of Web Development with Python, starting with an introduction to web concepts and the Flask framework.
Happy coding!