Part 4: Generators

Generators give you an iterator object with no access to the underlying data … if it even exists.  Conceptually, iterators are about various ways to loop over data.  They can generate data on the fly.  In general, you can use either an iterator or a generator — in fact, a generator is a type of iterator.  Generators do some of the book-keeping for you and therefore involve simpler syntax.

yield

def a_generator_function(params):
    some_stuff
    yield something

Generator functions “yield” a value, rather than returning a value. It *does* ‘return’ a value, but rather than ending execution of the function, it preserves function state so that it can pick up where it left off.  In other words, state is preserved between yields.
A function with yield  in it is a factory for a generator. Each time you call it, you get a new generator:
gen_a = a_generator()
gen_b = a_generator()

Each instance keeps its own state.
To master yield, you must understand that when you call the function, the code you have written in the function body does not run.  The function only returns the generator object.  The actual code in the function is run when next() is called on the generator itself.
An example: an implementation of range() as a generator:
def y_range(start, stop, step=1):
    i = start
    while i < stop:
        yield i
        i += step

Generator Comprehensions: yet another way to make a generator:
>>> [x * 2 for x in [1, 2, 3]]
[2, 4, 6]
>>> (x * 2 for x in [1, 2, 3])
<generator object <genexpr> at 0x10911bf50>
>>> for n in (x * 2 for x in [1, 2, 3]):
...   print n
... 2 4 6