Search code examples
pythonpython-3.xgeneratoryieldyield-from

Why does nesting "yield from" statements (generator delegation) produce terminating `None` value?


Is it possible to nest yield from statements?

The simple form is obvious:

def try_yield1():
    x = range(3)
    yield from x

Which produces:

0
1
2

But what if I have nested generators?

def try_yield_nested():
   x = [range(3) for _ in range(4)]
    yield from ((yield from y) for y in x)

This produces:

0
1
2
None # why?
0
1
2
None # ...
0
1
2
None # ...

Why does it produce None if I used yield from (even though it is nested)?

I know I can do something like:

from itertools import chain

def try_yield_nested_alternative():
    x = [range(3) for _ in range(4)]
    yield from chain.from_iterable(x)

Which produces the same output leaving out the None (which is what I expect). I can also write a simple loop:

for x in [range(3) for _ in range(3)]:
    yield from x

But, I thought it would be more pythonic to use nested delegation (preferably even yield from x from y or yield from x for x in y, but that is not proper syntax). Why is it not working as I expect?


Solution

  • yield is an expression, it'll evaluate whatever you send(value) it. Simply put, if you send it nothing, you'll get a None output because the value of yield would be None.

    yield from itself is equal to None, and you're using two of them. At the second yield from you're iterating a list, which inputs the generator with the items of that list, but you're also using yield from to return that generator expression, which returns the list and also itself which is equivalent to None at each iteration, thus why after each item iteration of range you get a None.

    Essentially this is what is happening:

    1. (yield from y) for y in x yields the values, [0, 1, 2]
    2. yield from yields the values from the previous, and also yield from from previous which is None.
    3. Evaluating to [0, 1, 2] and then adding a None due to yield from in (yield from y) for y in x.

    Unfortunately, there is no way to get rid of the None output, due to the nature of the expression. You're better off using from_iterable.

    Source explaining yield expression; https://docs.python.org/2.5/ref/yieldexpr.html