Articles

Closures in Python

4 min read

Closures in Python are created when a nested function references a variable from its containing function. The inner function has access to its own local scope, the scope of the outer function, and even the global scope.

It “closes over” the variables it needs, hence the name closure. This behavior enables functions to have private, encapsulated data that can be accessed and manipulated through the closure.

One of the main benefits of closures is their ability to create and return specialized functions on the fly. This is particularly useful when we want to define functions that have certain behavior but need to be parameterized with different values.

Python Closures provides a way to generate these customized functions without having to define them explicitly.

Python Nested Functions

In Python, nested functions refer to the concept of defining a function inside another function. This means that a function is declared and defined within the body of another function, creating a nested or inner function.

The nested function can access variables and attributes from its enclosing function’s scope, as well as the global scope.

Example of nested function Python :

def outer_function():
    x = 10
    
    def inner_function():
        y = 5
        result = x + y
        print(f"The result is {result}")
    
    inner_function()

outer_function()

Here, we have an outer_function that defines a local variable x with a value of 10.

Inside outer_function, we have another function called, which defines its own local variable y with a value of 5.

The inner_function accesses the x variable from its enclosing scope and performs an addition operation with y.

Finally, it prints the result. When we call outer_function, it invokes the inner_function as well, resulting in the output: “The result is 15”.

Python Closures

A closure is a function object that remembers values in the enclosing scope even if they are not present in memory.

It allows a nested function to access and retain the values of variables defined in its enclosing function, even after the outer function has finished executing.

This behavior makes closures incredibly powerful for maintaining state and creating functions with persistent data.

Example 1: Counter using closures

def counter():
    count = 0
    
    def increment():
        nonlocal count
        count += 1
        print(count)
    
    return increment

# Create two separate counter instances
counter1 = counter()
counter2 = counter()

counter1()  # Output: 1
counter1()  # Output: 2

counter2()  # Output: 1

Here, the counter function returns the increment function. The count variable is defined in the counter function’s scope and is accessible within the increment function due to the closure.

Each time increment is called, it increments the count and prints the updated value.

The closure allows the count variable to retain its value between multiple calls to the increment function, resulting in separate counters for counter1 and counter2.

Example 2: Function factory using closures

def multiply_by(n):
    def multiplier(x):
        return x * n
    return multiplier

# Create a function that multiplies by 5
multiply_by_5 = multiply_by(5)

result = multiply_by_5(10)
print(result)  # Output: 50

Here, the multiply_by function takes a parameter n and returns the multiplier function. The multiplier function is a closure that remembers the value of n even after the multiply_by function has been completed. The returned multiplier function multiplies its input x by the value of n, effectively creating a specialized function that multiplies by a specific number.

Use of Closures in Python

  • Data Encapsulation: Closures enable data encapsulation by allowing inner functions to access and modify variables from their enclosing scope. This can be used to create private variables and control access to them.
def create_counter():
    count = 0
    
    def increment():
        nonlocal count
        count += 1
        print(count)
    
    def decrement():
        nonlocal count
        count -= 1
        print(count)
    
    return increment, decrement

inc, dec = create_counter()
inc()  # Output: 1
inc()  # Output: 2
dec()  # Output: 1
  • Function Factories: Closures allow for the creation of function factories, where a function generates and returns customized functions based on certain parameters or configurations.
def multiply_by(n):
    def multiplier(x):
        return x * n
    return multiplier

multiply_by_5 = multiply_by(5)
result = multiply_by_5(10)
print(result)  # Output: 50
  • Memoization: Python Closures can be used to implement memoization, a technique that caches the results of expensive function calls to improve performance by avoiding redundant computations.
def memoize(func):
    cache = {}
    
    def wrapper(*args):
        if args in cache:
            return cache[args]
        result = func(*args)
        cache[args] = result
        return result
    
    return wrapper

@memoize
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(10))  # Output: 55
  • Decorators: Closures form the basis for creating decorators, which are functions that modify or enhance the behavior of other functions without modifying their source code directly.
def logger(func):
    def wrapper(*args, **kwargs):
        print(f"Calling function: {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Function {func.__name__} returned: {result}")
        return result
    return wrapper

@logger
def add(a, b):
    return a + b

result = add(2, 3)  # Output: Calling function: add
                    #         Function add returned: 5

 


Leave a Reply

Your email address will not be published. Required fields are marked *