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