Introduction
Closures are an important concept in Python that allow functions to preserve their execution context. This enables powerful constructs like factories, decorators, and helps impart state to functions. In this tutorial, we will learn about closures in detail with simple examples.
What are Closures in Python?
A closure is an inner or nested function that carries information about its enclosing scope, even when invoked outside of that scope. The enclosed scope consists of any non-global variables available in the enclosing function at the time the nested function was defined.
This is possible due to late binding in Python. The values of variables used in closures are looked up dynamically at runtime rather than being fixed at compile time.
A Simple Example of Closure
Let’s look at a basic example to understand closures:
def print_msg(msg):
def printer():
# printer() is a closure
print(msg)
printer()
print_msg("Hello") # Output: Hello
Here print_msg()
is the outer enclosing function while printer()
is the nested function that forms the closure.
The closure printer()
captures the msg
variable from the enclosing scope of print_msg()
. When print_msg()
is invoked, the value “Hello” gets bound to msg
.
The closure printer()
retains its binding to msg
even when called outside of print_msg()
. Hence it correctly prints the message.
Example 2: Counter using a Closure
def counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter1 = counter()
counter2 = counter()
print(counter1()) # 1
print(counter1()) # 2
print(counter2()) # 1
Example 3: Custom Sorting with Closures
def custom_sort(key_func):
def sort_list(lst):
return sorted(lst, key=key_func)
return sort_list
students = [
{'name': 'Alice', 'score': 85},
{'name': 'Bob', 'score': 92},
{'name': 'Charlie', 'score': 78}
]
sort_by_name = custom_sort(lambda x: x['name'])
sort_by_score = custom_sort(lambda x: x['score'])
sorted_by_name = sort_by_name(students)
sorted_by_score = sort_by_score(students)
print(sorted_by_name)
print(sorted_by_score)
Returning Closures from Functions
Closures can be returned from functions as well:
def power_factory(exp):
def pow_gen(base):
return base ** exp
return pow_gen # Returns a closure
square = power_factory(2)
cube = power_factory(3)
print(square(5)) # 25
print(cube(2)) # 8
Here power_factory()
returns a closure pow_gen
that captures the exp
argument. This gets bound to 2 and 3 respectively when called. The closures square
and cube
retain their binding and generate the right powers.
When Variables Get Captured
It is important to note that closures capture variables, not values. If a variable binding changes between defining and calling the closure, the current binding is captured:
msg = "Hello"
def printer():
print(msg)
printer() # Hello
msg = "World"
printer() # World
Since msg
changed before calling printer()
again, the new value got captured.
Uses of Closures
Some common uses of closures are:
- Parameterizing functions e.g. factories
- Saving state between function calls
- Implementing decorators
- Creating custom generators
Overall they allow crafting functions that are more intelligent while avoiding use of global state.
Key Benefits of Closures
- Avoid expensive re-creation of dynamic contexts
- Reduce code repetition through factories
- Enable better state retention
- Impart modularity by encapsulating logic
Closures in Python are a powerful feature that is useful across many problem domains.
Conclusion
Closures in python allow functions to retain and access their creation context. This provides continuity between calls and helps reduce global state reliance. In Python, functions dynamically capture variables rather than values at definition time due to late binding. Closures in python are useful for parameterization via factories, state retention, encapsulation, and other applications. They showcase Python’s support for powerful functional programming constructs in an elegant way.
Frequently Asked Questions
- What is a closures in Python?
A closure is an inner or nested function that remembers values in the enclosing scope even when invoked externally. The nested function retains access to the enclosing scope and its non-global variables.
- How do closures work in Python?
Python implements closures via late binding. The values of outer scope variables are looked up dynamically at call time rather than being fixed at compile time. This allows the inner function to capture the runtime context.
- When are closures created in Python?
Closures are created whenever a nested function references one or more variables from its enclosing function scope. The nested function and its bindings get defined and stored to create the closure.
- What are some common uses of closures in Python?
Typical uses of closures include parameterizing functions via factory methods, retaining state between calls to simulate private methods, implementing decorators, creating custom generators etc.
- What are the benefits of using closures in Python?
Benefits include avoiding expensive re-creation of dynamic function contexts, reducing code repetition through factories, enabling better state retention between calls, and encapsulating logic neatly.
- What gets captured in a closure in Python?
A closure captures variable bindings rather than values. The current binding value is captured at the time of call dynamically per late binding semantics.
- Can closures modify enclosing scope variables in Python?
Yes, closures can modify the bindings of outer scope variables since they have access to the enclosing state. This allows using closures to update state.
- Do closures capture global variables in Python?
Closures only capture non-global variables from their enclosing function scope. Global variables remain global even within closures.
- What is a closure disadvantage in Python?
Retaining enclosing scope references can result in extra memory usage if the closures outlive the enclosing function.
- How can factory methods use closures for parameterization?
The factory returns a parameterized closure capturing factory arguments, while callers get appropriately configured functions.