Understanding Closures and Decorators in Python

Variable Scope and Nested Functions

Function Basics

Functions are defined using the def keyword, followed by a name and parentheses. The body is indented and may include an optional return statement.

def calculate_total(price, tax_rate):
    """Compute total cost including tax."""
    return price * (1 + tax_rate)

final_cost = calculate_total(100, 0.08)
print(final_cost)  # Output: 108.0

Variable Scope

Variables defined inside a function are local. Those defined outside are global and accessible throughout the program.

global_data = "Accessible everywhere"

def show_data():
    local_data = "Only inside function"
    print(global_data)  # Works
    print(local_data)   # Works

show_data()
# print(local_data)  # Error: NameError

To modify a global variable within a function, use the global keyword.

counter = 0

def increment():
    global counter
    counter += 1

increment()
print(counter)  # Output: 1

Nested Functions and nonlocal

Functions can be defined inside other functions. The nonlocal keyword allows an inner function to modify a variable from an enclosing (non-global) scope.

def outer_container():
    value = 10
    def inner_modifier():
        nonlocal value
        value *= 2
        return value
    return inner_modifier

mod_func = outer_container()
print(mod_func())  # Output: 20
print(mod_func())  # Output: 40

Closures

A closure is a nested function that captures and remembers values from its enclosing scope, even after the outer function has finished executing.

Core Components of a Closure

  1. A nested function.
  2. The nested function references a variable from its enclosing scope.
  3. The outer function returns the nested function.
def create_greeter(greeting):
    def greet(name):
        print(f"{greeting}, {name}!")
    return greet

say_hi = create_greeter("Hi")
say_hi("Alice")  # Output: Hi, Alice!

Here, the inner function greet retains access to the greeting variable from create_greeter.

Multi-Level Closure Example

def level_one(a):
    def level_two(b):
        def level_three(c):
            return a + b + c
        return level_three
    return level_two

func_a = level_one(5)
func_b = func_a(10)
print(func_b(15))  # Output: 30

The final function func_b (which is level_three) remembers the values a=5 and b=10 from its outer scopes.

Decorators

Functions as First-Class Objects

In Python, functions are objects that can be assigned, passed as arguments, returnde, and stored in data structures.

def shout(text):
    return text.upper()

# Assign function to a variable
yell = shout
print(yell("hello"))  # Output: HELLO

# Store functions in a list
operations = [str.lower, str.upper]
for op in operations:
    print(op("TeSt"))  # Output: test, TEST

Higher-Order Functions

Higher-order functions either take a function as an argument or return a function.

def apply_operation(func, data):
    return [func(item) for item in data]

def cube(x):
    return x ** 3

numbers = [1, 2, 3]
cubed = apply_operation(cube, numbers)
print(cubed)  # Output: [1, 8, 27]

Basic Decorator

A decorator is a function that wraps another function to extend its behavior without modifying its source code.

def simple_decorator(original_func):
    def wrapper():
        print("Action before function.")
        original_func()
        print("Action after function.")
    return wrapper

@simple_decorator
def display_message():
    print("Core function executing.")

display_message()
# Output:
# Action before function.
# Core function executing.
# Action after function.

The @simple_decorator syntax is equivalent to display_message = simple_decorator(display_message).

Decorators with Arguments

Decorators can accept arguments themselves, requiring an extra level of nesting.

def tag_decorator(tag_name):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"<{tag_name}>") 
            result = func(*args, **kwargs)
            print(f"</{tag_name}>") 
            return result
        return wrapper
    return decorator

@tag_decorator("div")
def render_content(text):
    print(text)

render_content("Page Content")
# Output:
# <div>
# Page Content
# </div>

Class-Based Decorators

A class can act as a decorator if it implements the __call__ method.

class CounterDecorator:
    def __init__(self, func):
        self.func = func
        self.call_count = 0

    def __call__(self, *args, **kwargs):
        self.call_count += 1
        print(f"Function '{self.func.__name__}' called {self.call_count} time(s).")
        return self.func(*args, **kwargs)

@CounterDecorator
def compute_sum(a, b):
    return a + b

print(compute_sum(3, 4))  # Output: Function 'compute_sum' called 1 time(s).\n7
print(compute_sum(5, 6))  # Output: Function 'compute_sum' called 2 time(s).\n11

The __call__ method enables an instance to be called like a function.

class Multiplier:
    def __call__(self, x, y):
        return x * y

double = Multiplier()
print(double(7, 3))  # Output: 21

The @property Decorator

The @property decorator allows class methods to be accessed like attributes, enabling getter, setter, and deleter functionality.

class Product:
    def __init__(self, cost):
        self._cost = cost
        self._markup = 1.5  # 50% markup

    @property
    def price(self):
        """Getter for the calculated price."""
        return self._cost * self._markup

    @price.setter
    def price(self, new_price):
        """Setter adjusts the base cost."""
        self._cost = new_price / self._markup

    @price.deleter
    def price(self):
        """Deleter resets the markup."""
        self._markup = 1.0

item = Product(100)
print(item.price)      # Output: 150.0
item.price = 180       # Uses setter
print(item._cost)      # Output: 120.0
del item.price         # Uses deleter
print(item.price)      # Output: 120.0

Tags: python closures decorators functions programming

Posted on Fri, 08 May 2026 05:27:12 +0000 by jtgraphic