Chapter 15 of 21
List Comprehensions & Generator Expressions
Current Study Module
Decorators, Iterators & Generators
Datetime & Temporal Arithmetic
Chapter 14 • Advanced Metaprogramming

Decorators, Generators, Iterators & Context Managers

Metaprogramming enables you to inspect, modify, and extend code dynamically. Learn how decorators inject logging, caching, and authentication into functions, why @functools.wraps is mandatory, how the Iterator Protocol powers loops, and how generators enable infinite data streams with yield.

1. How Decorators Work Under the Hood

A decorator is a higher-order function that takes a function as input, extends its behavior, and returns a new wrapper function. Writing @my_decorator above a function is syntactic sugar for:func = my_decorator(func).

import time
import functools

# Production execution timer decorator
def timing_decorator(func):
    # @functools.wraps preserves original function's __name__, docstrings, and signature!
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.perf_counter()
        result = func(*args, **kwargs)
        duration = time.perf_counter() - start_time
        print(f"[PERF] {func.__name__} executed in {duration:.4f} seconds.")
        return result
    return wrapper

@timing_decorator
def fetch_api_records(batch_size: int):
    """Simulates fetching external data."""
    time.sleep(0.15)
    return [f"record_{i}" for i in range(batch_size)]

records = fetch_api_records(500)
print("Function Name Preserved:", fetch_api_records.__name__)  # 'fetch_api_records' (not 'wrapper')

2. Parameterized Decorators (Decorators with Arguments)

To pass configuration arguments into a decorator (e.g. @retry(max_attempts=3)), you add an outer factory function that returns the actual decorator:

def retry(max_attempts: int = 3, delay_sec: float = 0.5):
    """Decorator factory that retries failing functions."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            attempts = 0
            while attempts < max_attempts:
                try:
                    return func(*args, **kwargs)
                except Exception as err:
                    attempts += 1
                    print(f"Attempt {attempts} failed: {err}. Retrying in {delay_sec}s...")
                    time.sleep(delay_sec)
            raise RuntimeError(f"{func.__name__} failed after {max_attempts} attempts.")
        return wrapper
    return decorator

@retry(max_attempts=2, delay_sec=0.1)
def unstable_network_call():
    raise ConnectionResetError("Socket timeout")

# Calling it will retry twice before raising RuntimeError!

3. Generators & State Suspension with "yield"

When a regular function encounters return, its stack frame is destroyed and local variables are freed. When a generator encounters yield, execution is suspended, the state frame is frozen in RAM, and control returns to the caller until next() is invoked:

# Infinite Fibonacci Generator (Consumes constant memory!)
def fibonacci_stream():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci_stream()
first_six = [next(fib) for _ in range(6)]
print("First 6 Fibonacci numbers:", first_six)  # [0, 1, 1, 2, 3, 5]

# Delegating to sub-generators with 'yield from'
def combined_pipeline():
    yield from ["stage_1_init", "stage_1_clean"]
    yield from ["stage_2_embed", "stage_2_save"]

for step in combined_pipeline():
    print("Pipeline Step:", step)

4. Custom Context Managers with @contextlib.contextmanager

Instead of writing a boilerplate class with __enter__ and __exit__, the standard library provides @contextmanager to convert a single generator into a with block:

from contextlib import contextmanager

@contextmanager
def temporary_database_transaction(db_connection):
    print("BEGIN TRANSACTION;")
    try:
        yield db_connection  # Code inside the 'with' block runs here!
        print("COMMIT TRANSACTION;")
    except Exception as err:
        print(f"ROLLBACK TRANSACTION due to error: {err}")
        raise

# Usage:
# with temporary_database_transaction(conn) as db:
#     db.insert_records(data)

Explore Related Tools

Hand-picked utilities and calculators related to this tool.

Related Tools