Decorators and Generators Masterclass

Module 2 • Session 20 • Comprehensive Guide

decorators generators yield-statement functools-wraps lazy-evaluation

Decorators and Generators represent two of Python's most elegant paradigms for advanced control flow. Decorators provide meta-programming capabilities to extend function behaviour without modifying source code, while Generators offer lazy evaluation to stream dataset sequences with minimal memory overhead.

1. Higher-Order Functions & Closures

In Python, functions are first-class objects. They can be assigned to variables, passed as arguments, and returned from other functions. A closure occurs when an inner function retains access to variables defined in its enclosing scope, even after the outer function finishes execution.

# First-Class Functions and Closures def make_multiplier(factor): def multiply(number): return number * factor # 'factor' is captured from outer scope return multiply double = make_multiplier(2) triple = make_multiplier(3) print("Double 5:", double(5)) # Output: 10 print("Triple 5:", triple(5)) # Output: 15

2. Python Decorators Syntax & Design Patterns

A decorator takes a target function as input, wraps it with additional behavior (logging, authentication, timing), and returns the wrapper callable. The @decorator syntax is syntactic sugar for func = decorator(func).

import time from functools import wraps # Execution Time Measurement Decorator def execution_timer(func): @wraps(func) # Preserves func.__name__ and func.__doc__ def wrapper(*args, **kwargs): start_time = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - start_time print(f"[TIMER] '{func.__name__}' executed in {elapsed:.6f} seconds.") return result return wrapper @execution_timer def compute_sum(n): """Calculates sum of integers from 1 to n.""" return sum(range(1, n + 1)) print("Result:", compute_sum(1000000)) print("Function Name Preserved:", compute_sum.__name__)

Parameterized Decorators

Passing arguments to decorators requires adding an outer factory level to accept arguments and return the actual decorator.

# Decorator taking arguments (Retry mechanism) def repeat(num_times): def decorator_repeat(func): @wraps(func) def wrapper(*args, **kwargs): for _ in range(num_times): result = func(*args, **kwargs) return result return wrapper return decorator_repeat @repeat(num_times=3) def greet(name): print(f"Hello, {name}!") greet("Alice")

3. Generators and the yield Statement

Unlike standard functions that execute and return a single final value via return, a Generator function contains one or more yield statements. When called, it returns a generator iterator that yields execution values sequentially on demand.

# Custom Fibonacci Generator def fibonacci_generator(limit): a, b = 0, 1 count = 0 while count < limit: yield a a, b = b, a + b count += 1 # Consuming Generator Values via Iteration fib_gen = fibonacci_generator(8) print("Fibonacci Sequence:", [num for num in fib_gen]) # Manual Consumption via next() single_gen = fibonacci_generator(3) print("Manual Next 1:", next(single_gen)) # 0 print("Manual Next 2:", next(single_gen)) # 1 print("Manual Next 3:", next(single_gen)) # 1 # calling next() again raises StopIteration exception

4. Decorators vs. Generators Comparison

Concept Primary Purpose Key Syntax / Keywords Core Advantage
Decorator Modify, wrap, or extend the behavior of existing callables. @decorator_name, @wraps DRY principle, clear separation of concerns (logging, auth, caching).
Generator Stream calculated output values lazily over time. yield, yield from Memory efficiency $O(1)$, process unbounded or massive datasets safely.

🏋️ Try It Yourself: Practice Challenges

Challenge 1: Memoization / Caching Decorator

Build a custom decorator `@memoize` that caches function results in an internal dictionary to speed up expensive recursive calls like computing Fibonacci numbers.

Challenge 2: Infinite Prime Generator

Write a generator function `infinite_primes()` that indefinitely yields prime numbers on demand without running out of bounds.

Challenge 3: Role-Based Authorization Decorator

Implement a parameterized decorator `@require_role("admin")` that checks user authorization before permitting wrapper execution.

Challenge 4: Pipeline Data Stream Generator

Create two chaining generator functions: one to generate random integer log entries, and another to filter and format warning logs on the fly.

Challenge 5: Rate Limiting Decorator

Design a decorator `@rate_limit(max_calls=3, period=5)` that limits execution frequency to a maximum count within a time window.

Challenge 6: Subgenerator Delegation (`yield from`)

Write nested generator functions that utilize `yield from` syntax to flatten multiple nested sequences into a single continuous sequence.

⚡ Interactive Sandbox (Decorators and Generators in Python (Functions, Yield & Iterators))
Console Output:
Click "Run Code" above to execute interactive scripts...

📝 Knowledge Check Quiz

1. What primary purpose does `@functools.wraps(func)` serve inside decorator wrappers?
2. What happens to local state variables in a generator function when a `yield` statement is reached?
3. What exception is raised automatically when a generator exhausts all `yield` statements?
Advertisement
Horizontal Ad Banner Slot (728x90 / Responsive)