Creating generators with yield
Generators provide a simpler way to create iterators without needing to define a class with __iter__() and __next__() methods. A generator function is defined like a normal function but uses the yield keyword instead of return. When called, it returns a generator object that supports the iterator protocol. Each time next() is called on the generator object, the function executes until it hits a yield statement, which provides a value and pauses the function's execution. The function's state (variables, instruction pointer) is saved and restored when execution resumes. Generators are memory efficient because they generate values on-the-fly rather than storing all values in memory. They're particularly useful for processing large datasets, generating infinite sequences, and implementing pipelines. Generator functions can have multiple yield statements and can use loops. The yield from syntax (Python 3.3+) allows delegating to another generator.