What does a yield inside a yield do?
Consider the following code:
def mygen():
yield (yield 1)
a = mygen()
print(next(a))
print(next(a))
The output yields:
1
None
What does the interpreter do at the "outside" yield exactly?
Solution 1:
a
is a generator object. The first time you call next
on it, the body is evaluated up to the first yield
expression (that is, the first to be evaluated: the inner one). That yield
produces the value 1
for next
to return, then blocks until the next entry into the generator. That is produced by the second call to next
, which does not send any value into the generator. As a result, the first (inner) yield
evaluates to None
. That value is used as the argument for the outer yield
, which becomes the return value of the second call to next
. If you were to call next
a third time, you would get a StopIteration
exception.
Compare the use of the send
method (instead of next
) to change the return value of the first yield
expression.
>>> a = mygen()
>>> next(a)
1
>>> a.send(3) # instead of next(a)
3
>>> next(a)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
A more explicit way of writing the generator would have been
def mygen():
x = yield 1
yield x
a = mygen()
print(a.send(None)) # outputs 1, from yield 1
print(a.send(5)) # makes yield 1 == 5, then gets 5 back from yield x
print(a.send(3)) # Raises StopIteration, as there's nothing after yield x
Prior to Python 2.5, the yield
statement provided one-way communication between a caller and a generator; a call to next
would execute the generator up to the next yield
statement, and the value provided by the yield
keyword would serve as the return value of next
. The generator
would also suspend at the point of the yield
statement, waiting for the next call to next
to resume.
In Python 2.5, the yield
statement was replaced* with the yield
expression, and generators acquired a send
method. send
works very much like next
, except it can take an argument. (For the rest of this, assume that next(a)
is equivalent to a.send(None)
.) A generator starts execution after a call to send(None)
, at which point it executes up to the first yield
, which returns a value as before. Now, however, the expression blocks until the next call to send
, at which point the yield
expression evaluates to the argument passed to send
. A generator can now receive a value when it resumes.
* Not quite replaced; kojiro's answer goes into more detail about the subtle difference between a yield
statement and yield
expression.
Solution 2:
yield
has two forms, expressions and statements. They're mostly the same, but I most often see them in the statement
form, where the result would not be used.
def f():
yield a thing
But in the expression form, yield
has a value:
def f():
y = yield a thing
In your question, you're using both forms:
def f():
yield ( # statement
yield 1 # expression
)
When you iterate over the resulting generator, you get first the result of the inner yield expression
>>> x=f()
>>> next(x)
1
At this point, the inner expression has also produced a value that the outer statement can use
>>> next(x)
>>> # None
and now you've exhausted the generator
>>> next(x)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
To understand more about statements vs expressions, there are good answers in other stackoverflow questions: What is the difference between an expression and a statement in Python?
Solution 3:
>>> def mygen():
... yield (yield 1)
...
>>> a = mygen()
>>>
>>> a.send(None)
1
>>> a.send(5)
5
>>> a.send(2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
>>>
>>>
>>> def mygen():
... yield 1
...
>>> def mygen2():
... yield (yield 1)
...
>>> def mygen3():
... yield (yield (yield 1))
...
>>> a = mygen()
>>> a2 = mygen2()
>>> a3 = mygen3()
>>>
>>> a.send(None)
1
>>> a.send(0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> a2.send(None)
1
>>> a2.send(0)
0
>>> a2.send(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> a3.send(None)
1
>>> a3.send(0)
0
>>> a3.send(1)
1
>>> a3.send(2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
Every other yield simply waits for a value to be passed into, generator don't only give data but they also receive it.
>>> def mygen():
... print('Wait for first input')
... x = yield # this is what we get from send
... print(x, 'is received')
...
>>> a = mygen()
>>> a.send(None)
Wait for first input
>>> a.send('bla')
bla is received
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
yield
gives the next value when you continue if you get it, and if it is not used for giving the next value, it is being used for receiving the next
>>> def mygen():
... print('Wait for first input')
... x = yield # this is what we get from send
... yield x*2 # this is what we give
...
>>> a = mygen()
>>> a.send(None)
Wait for first input
>>> a.send(5)
10
>>>
Solution 4:
Any generator exhausts elements till it runs out of them.
In the 2-level nested example like below, the first next
gives us the element from inner most yield, which is 1, the next yields just returns None
, since it has no elements to return, if you call next
again, it will return StopIteration
def mygen():
yield (yield 1)
a = mygen()
print(next(a))
print(next(a))
print(next(a))
You can expand this case to include more nested yields, and you will see that after n
next
are called, StopIteration
expection is thrown, below is an example with 5 nested yields
def mygen():
yield ( yield ( yield ( yield (yield 1))))
a = mygen()
print(next(a))
print(next(a))
print(next(a))
print(next(a))
print(next(a))
print(next(a))
Note that this answer is just based on my observation, and might not be technically correct in the nitty-gritties, all updates and suggestions are welcome