Reputation: 21
I came across a funny behavior of yield
today that I don't really understand. Here's my code:
def a():
def b(x):
print("entering b.")
yield 0
if x == 0:
print("calling b.")
b(x + 1)
print("return from b.")
print("leaving b.")
for x in b(0):
yield x
for x in a():
print(x)
That outputs:
entering b.
0
calling b.
return from b.
leaving b.
What quite confuses me is that explicitly calling b(x + 1)
does not call b
(!), neither does Python give any error or exception.
Now, obviously the error in the code above is that b(x + 1)
should really yield the value that b
yields - so it should read something like:
for x in b(x + 1):
yield x
Things work then.
Still, is this something with yield
I should be aware of?
Upvotes: 2
Views: 317
Reputation: 488183
The answer you got so far is right (and I've upvoted it), but I see you're still fighting with this a bit, so let's try this variant:
def a():
def b(x):
print("entering b.")
yield 0
if x == 0:
print("calling b.")
temp = b(x + 1)
print("calling b resulted in temp =", temp)
print("return from b.")
print("leaving b.")
for x in b(0):
yield x
for x in a():
print(x)
Now let's run this in Python 3.x:
entering b.
0
calling b.
calling b resulted in temp = <generator object a.<locals>.b at 0x800ac9518>
return from b.
leaving b.
That is, temp
is set to the result of calling b(x + 1)
, and that result is this <generator object ...>
thing.
You then have to do something with the generator object, so here is yet a third variant:
def a():
def b(x):
print("entering b.")
yield 0
if x == 0:
print("calling b.")
temp = b(x + 1)
print("calling b resulted in temp =", temp)
y = next(temp)
print("by doing next(temp), I got", y)
print("return from b.")
print("leaving b.")
for x in b(0):
yield x
for x in a():
print(x)
Running this produces:
entering b.
0
calling b.
calling b resulted in temp = <generator object a.<locals>.b at 0x800ac9518>
entering b.
by doing next(temp), I got 0
return from b.
leaving b.
The yield from
variant in the other answer basically means "keep calling temp and yielding whatever it yields, until it says it's done". This y = next(temp)
called temp just once.
Exercise for the reader: Try the fourth variant quoted below. Try to predict, before you run it, what you'll see. Do you see what you predicted?
def a():
def b(x):
print("entering b.")
yield 0
if x == 0:
print("calling b.")
temp = b(x + 1)
print("calling b resulted in temp =", temp)
y = next(temp)
print("by doing next(temp), I got", y)
try:
print("about to re-enter temp")
y = next(temp)
print("with the second next(temp), I got", y)
except StopIteration:
print("with the second next(temp), I got StopIteration")
print("return from b.")
else:
print("b had x =", x)
print("leaving b.")
for x in b(0):
yield x
for x in a():
print(x)
Upvotes: 3
Reputation: 6302
The b(x + 1)
is called, but not executed until yielded in the context of the calling function.
Using yield from
to yield all the values produced by that call to b()
and execute the body:
def a():
def b(x):
print("entering b.")
yield 0
if x == 0:
print("calling b.")
yield from b(x + 1)
print("return from b.")
print("leaving b.")
for x in b(0):
yield x
for x in a():
print(x)
Upvotes: 5