Home » Python Closures: A Tutorial

Python Closures: A Tutorial

Spring Framework Basics Video Course
Java SE 11 Programmer I [1Z0-815] Practice Tests
Oracle Java Certification
Java SE 11 Developer (Upgrade) [1Z0-817]
1 Year Subscription
Java SE 11 Programmer II [1Z0-816] Practice Tests

In Python, closures are functions that can retain access to variables from their enclosing scope, even after that scope has finished executing.

Closures are useful when you want to create function factories or keep data encapsulated.

What is a Closure?

A closure is a function object that remembers values in enclosing scopes even if they are not present in memory anymore. A closure typically involves:

A nested function (a function defined inside another function).
The nested function accesses variables from its containing (enclosing) function.
The enclosing function has finished execution, but the nested function retains access to the variables from the enclosing scope.

Key Components of a Closure

Outer Function: Defines a local variable and returns the inner function.
Inner Function: Uses the local variable of the outer function and may be returned to be used later.

Example of a Basic Closure

def outer_function(message):
    # Outer function defines a local variable
    def inner_function():
        # Inner function uses the outer function's variable
        print(message)
    
    # Return the inner function
    return inner_function

# Create a closure by calling the outer function
closure = outer_function("Hello from the closure!")

# Call the closure function
closure()  # Output: Hello from the closure!

How Does This Work?

outer_function takes a parameter message and defines an inner function inner_function that prints message.
The outer_function returns the inner_function, which remembers the value of message even though the outer_function has finished execution.
When closure() is called, it still has access to the message variable from the outer function, thanks to the closure.

1. Practical Example: Creating Function Factories

A common use case for closures is creating function factories. These are functions that generate other functions with specific behavior.

Example: A Function Factory to Create Multipliers

def make_multiplier(x):
    # This outer function takes a multiplier 'x'
    def multiplier(n):
        # The inner function multiplies 'n' by 'x'
        return x * n
    return multiplier

# Create multiplier functions
double = make_multiplier(2)
triple = make_multiplier(3)

# Test the multiplier functions
print(double(5))  # Output: 10 (2 * 5)
print(triple(5))  # Output: 15 (3 * 5)

In this example, the make_multiplier function generates different multiplier functions (like double and triple) based on the argument passed to it (2 for double and 3 for triple). These functions retain access to their respective multipliers, even after make_multiplier has finished execution.

2. Closures vs. Regular Functions

Closures allow you to “remember” data between function calls without using global variables or class instances. This makes them powerful tools for creating highly customizable and reusable code.

Example: Closures to Keep Track of Running Total

def running_total():
    total = 0
    def add_to_total(value):
        nonlocal total  # Allows access to the outer variable 'total'
        total += value
        return total
    return add_to_total

# Create a closure to keep track of a running total
tracker = running_total()

print(tracker(10))  # Output: 10
print(tracker(5))   # Output: 15
print(tracker(3))   # Output: 18

The nonlocal keyword allows the inner function to modify the total variable from the outer function.
Each call to tracker() updates and remembers the total value.

3. The nonlocal Keyword

In the previous example, we used the nonlocal keyword to modify a variable from the outer scope. Without it, the inner function would only have access to the value but wouldn't be able to modify it.

Example: Using nonlocal in a Closure

def counter():
    count = 0
    def increment():
        nonlocal count  # Allows modifying 'count' in the enclosing scope
        count += 1
        return count
    return increment

# Create a closure
counter_closure = counter()

# Increment the counter
print(counter_closure())  # Output: 1
print(counter_closure())  # Output: 2
print(counter_closure())  # Output: 3

Without nonlocal, attempting to modify count would raise an error, because Python would treat count as a local variable within increment().

4. Advantages of Closures

Encapsulation: Closures allow you to encapsulate logic and data within a function. The inner function can access and manipulate variables that are hidden from the outside world.
Stateful Functions: Closures can remember the state between function calls without using global variables or object attributes.
Customizable Functions: You can use closures to create highly customizable functions by defining behavior at runtime based on the outer function's arguments.

5. Common Use Cases for Closures

a) Callback Functions

Closures are often used as callback functions in scenarios where you need to pass a function that can retain context.

def create_callback(message):
    def callback():
        print(f"Callback triggered with message: {message}")
    return callback

# Create callback functions
callback1 = create_callback("Task 1 complete")
callback2 = create_callback("Task 2 complete")

# Trigger the callbacks
callback1()  # Output: Callback triggered with message: Task 1 complete
callback2()  # Output: Callback triggered with message: Task 2 complete

b) Memoization with Closures

Closures can be used for memoization, which is an optimization technique to cache results of expensive function calls.

def memoize_factorial():
    cache = {}  # Dictionary to store previous results
    def factorial(n):
        if n in cache:
            return cache[n]
        if n == 0:
            return 1
        result = n * factorial(n - 1)
        cache[n] = result  # Store the result in cache
        return result
    return factorial

# Create a memoized factorial function
factorial = memoize_factorial()

# Test the memoized factorial function
print(factorial(5))  # Output: 120
print(factorial(6))  # Output: 720

This memoize_factorial function remembers previously computed factorials, speeding up future calculations.

6. When to Use Closures

Closures are useful when:

You need to maintain state across function calls (e.g., counters, accumulators).
You want to hide data or encapsulate functionality within a function.
You need to create factory functions that produce other functions with specific behaviors.
You want to use memoization to optimize function calls by caching results.

7. Disadvantages of Closures

Complexity: For beginners, closures can be harder to understand than regular functions.
Memory Usage: If not used carefully, closures can retain references to large objects, leading to higher memory consumption.

Summary

A closure is a function that remembers the variables from its enclosing scope, even after the outer function has finished executing.
Closures are created by defining a nested function and returning it from the outer function.
The nonlocal keyword allows the inner function to modify variables in the outer function's scope.

Closures are useful for encapsulating data, maintaining state between function calls, creating custom function factories, and implementing optimization techniques like memoization.

You may also like

Leave a Comment

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More