w3resource

Understanding Python Closures: Concepts and Practical Examples

Introduction to Python closures Functions

Closures in Python are a powerful feature that allows a function to retain access to its enclosing scope's variables even after the outer function has finished execution. Closures are used to create functions with some pre-configured behavior, making them particularly useful in decorators and callback functions. This tutorial will focus on examples to help you understand how closures work with maximum practical code.

Example 1: Basic Closure Example

This example demonstrates a basic closure, where an inner function retains access to variables from its enclosing function even after the outer function has completed.

Code:

def outer_function(msg):
    # Outer function that takes a message as an argument
    def inner_function():
        # Inner function that references the outer function's variable
        print(msg)
    return inner_function  # Return the inner function

# Create a closure
closure = outer_function("Hello, World!")

# Call the closure
closure()  # Output: Hello, World! 

Explanation:

  • Outer Function: 'outer_function()' defines a local variable 'msg' and an inner function 'inner_function()' that accesses msg.
  • Returning the Inner Function: The outer function returns 'inner_function()' without executing it.
  • Closure: When we call 'closure()', it still has access to 'msg', even though 'outer_function()' has finished executing.

Example 2: Closure with a Counter

This example shows how closures can maintain state between function calls, demonstrated by a simple counter that remembers its value.

Code:

def make_counter():
    # Outer function that initializes a counter
    count = 0

    def increment():
        # Inner function that increments the counter
        nonlocal count
        count += 1
        return count

    return increment  # Return the inner function

# Create a counter closure
counter = make_counter()

# Call the counter multiple times
print(counter())  # Output: 1
print(counter())  # Output: 2
print(counter())  # Output: 3

Explanation:

  • Outer Function: 'make_counter()' initializes a variable count and defines an inner function 'increment()' that modifies count.
  • 'nonlocal' Keyword: 'nonlocal' is used to modify the count variable in the enclosing scope.
  • Closure: The counter closure maintains its own count state across multiple calls.

Example 3: Customizing Functions with Closures

This example demonstrates how closures can create customized functions, such as a multiplier, that carry specific behavior based on the outer function's arguments.

Code:

def multiplier(factor):
    # Outer function that takes a factor
    def multiply(number):
        # Inner function that multiplies a number by the factor
        return number * factor
    return multiply  # Return the inner function

# Create closures with different factors
double = multiplier(2)
triple = multiplier(3)

# Use the closures
print(double(5))  # Output: 10
print(triple(5))  # Output: 15

Explanation:

  • Outer Function: 'multiplier()' takes a 'factor' and defines an inner function 'multiply()' that multiplies a given number by this factor.
  • Closures: The 'double' and 'triple' closures are customized functions that multiply numbers by 2 and 3, respectively.

Example 4: Closures as Decorators

This example illustrates how closures can be used as decorators, adding additional functionality (like logging) to existing functions without modifying their code.

Code:

def logger(func):
    # Outer function that takes a function as an argument
    def log_wrapper(*args, **kwargs):
        # Inner function that logs the function call and then executes it
        print(f"Calling {func.__name__} with arguments: {args}, {kwargs}")
        return func(*args, **kwargs)
    return log_wrapper  # Return the inner function

# Applying the closure as a decorator
@logger
def add(x, y):
    return x + y

# Call the decorated function
print(add(3, 4))  # Output: Calling add with arguments: (3, 4), {}
                  #         7

Explanation:

  • Outer Function: 'logger()' takes a function as an argument and defines an inner function 'log_wrapper()' that adds logging behavior.
  • Decorator: The 'log_wrapper()' closure logs the call details and then calls the original function.
  • Using the Closure: The 'add()' function is decorated with the 'logger' closure, so it logs its arguments whenever it is called.

Example 5: Closures for Data Encapsulation

This example demonstrates using closures for data encapsulation, creating an account with operations that securely manage the balance without exposing it directly.

Code:

def account(initial_balance):
    # Outer function that initializes balance
    balance = initial_balance

    def get_balance():
        # Inner function to get the balance
        return balance

    def deposit(amount):
        # Inner function to deposit money
        nonlocal balance
        balance += amount
        return balance

    def withdraw(amount):
        # Inner function to withdraw money
        nonlocal balance
        if amount <= balance:
            balance -= amount
            return balance
        else:
            return "Insufficient funds"

    # Return a dictionary of functions for encapsulated operations
    return {"get_balance": get_balance, "deposit": deposit, "withdraw": withdraw}

# Create an account closure
my_account = account(100)

# Perform operations
print(my_account["get_balance"]())  # Output: 100
print(my_account["deposit"](150))    # Output: 250
print(my_account["withdraw"](200))   # Output: 50

Explanation:

  • Data Encapsulation: The 'account()' function encapsulates the balance and provides controlled access via closures.
  • Operations: The closures ('get_balance', 'deposit', 'withdraw') manipulate and access the balance securely without exposing it directly.
  • Using the Closures: Operations on the account are performed using the returned closures, ensuring data encapsulation.


Follow us on Facebook and Twitter for latest update.