Skip to main content
Closures, decorators, iterators, and generators are among Python’s most powerful advanced features. While they are highly effective on their own, combining them allows you to build elegant, memory-efficient, and easily monitorable applications. For example, you can use decorators to profile or rate-limit generator streams, or use closures to maintain state inside custom iteration logic.

Learning Objectives

After completing this lesson, you will be able to:
  • Apply decorators to generator functions to monitor, time, or log value generation.
  • Understand the execution flow differences when decorating normal functions vs. generator functions.
  • Combine multiple generators into data pipelines and manage them using decorators.
  • Use closures to configure custom iterators dynamically.

Pattern 1: Decorating Generators

Decorators are commonly used to log, time, or rate-limit functions. However, when you apply a decorator to a generator function, you must be careful about when code executes.

The Generator Decorator Nuance

Recall that calling a generator function does not execute its body; it immediately returns a generator object. If a standard decorator wraps a generator, the wrapper function will run when the generator is created, but not when values are yielded. To intercept the actual value generation, the decorator’s wrapper must iterate through the generator and yield the values.

Example: Yield Logger

Here is a decorator that logs every time a generator yields a value:
Output

Practice

Exercise 1

Predict the output of the following code:
Calling my_generator() immediately triggers the wrapper print_call which prints "Decorator Wrapper Called" and returns the generator object. The code inside my_generator() (printing "Generator Body Started") runs only when next(g) is called.

Pattern 2: Stateful Iterators with Closures

Closures can be used to dynamically configure custom iterators without defining a full class structure. By combining a closure with a generator, we can maintain configuration and iteration state cleanly.

Example: Configurable Fibonacci Generator

Output

Practice

Exercise 1

What is the main benefit of using a closure to create configured generators?
It allows you to enclose configuration values (like limit or multiplier) in the outer scope, keeping the generator function signature clean and modular.

Pattern 3: Decorated Data Pipelines

In real-world data science and machine learning applications, generators are often chained together to process large streams of data. Decorators can be used to monitor metrics (like execution time, memory usage, or record count) across the entire pipeline.

Example: Monitoring a Pipeline

Output

Check Your Understanding

Question 1 Why does a standard decorator that simply executes a wrapped function not work out-of-the-box for logging yields inside a generator?
Because a generator function returns a generator object immediately when called instead of running the code block. To log yields, the decorator’s wrapper function must actively iterate over the generator object and yield each item.
Question 2 True or False: Using closures to configure generators helps keep local variables separated across multiple instances.
True. Each outer function call creates a new scope (closure), ensuring that configuration states and variables do not leak or interfere with other generator instances.
Question 3 How do decorators help when building modular streaming pipelines using generators?
They allow you to add auxiliary tasks (like performance profiling, error handling, rate limiting, and item auditing) to each pipeline step cleanly without polluting the core data-transformation logic.