A generator function is a function that contains at least one yield statement and no return statements that take an expression. When a generator function is invoked it returns a generator iterator, which when iterated (e.g. by a for loop, or explicitly with next) runs through the body of the function, freezing its state and returning control to the caller on each yield statement (and in Python 3.3, yield from statements).
Flow control inside a Python function is always forwards; without hacks like setting the current frames f_lineno (as famously done by the (April Fool's) goto statement), the only way for control to reach an earlier point is to use a loop (for or while). So without a loop or yield from, the maximum number of times a generator iterator can be invoked is bounded by the number of yield statements within the generator function.
Note that it's easy to write a flatten that returns an iterator; taking the original solution and writing return iter(flatten(first) + flatten(rest)) would do. But that wouldn't be a generator iterator, and the function wouldn't be a generator function.
Here's an implementation that abuses f_lineno to give loopless iteration. Unfortunately it has to use import sys:
def current_frame():
i = None
def gen():
yield i.gi_frame.f_back
i = gen()
return next(i).f_back
class Loop(object):
jump = False
def __call__(self, frame, event, arg):
if self.jump:
frame.f_lineno = self.lineno
self.jump = False
return None if event == 'call' else self
def __enter__(self):
import sys
sys.settrace(self)
current_frame().f_back.f_trace = self
self.lineno = current_frame().f_back.f_lineno
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is None:
self.jump = True
else:
import sys
sys.settrace(None)
current_frame().f_back.f_trace = None
return exc_type is StopIteration
def flatten(x):
if isinstance(x, list):
if x:
first, rest = flatten(x[0]), flatten(x[1:])
with Loop():
yield next(first)
with Loop():
yield next(rest)
pass
else:
yield x