2

I came across a funny behavior ofyield 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 xfor x in a():    print(x)

That outputs:

entering b.0calling b.return from b.leaving b.

What quite confuses me is that explicitly callingb(x + 1) does not callb (!), neither does Python give any error or exception.

Now, obviously the error in the code above is thatb(x + 1) should really yield the value thatb yields - so it should read something like:

for x in b(x + 1):  yield x

Things work then.

Still, is this something withyield I should be aware of?

user24343's user avatar
user24343
91210 silver badges19 bronze badges
askedFeb 24, 2019 at 20:24
Bernhard Liebl's user avatar
6
  • 4
    You might wantyield from b(x + 1)CommentedFeb 24, 2019 at 20:27
  • 1
    Callingb doesn't run the body. That doesn't change just because it's a recursive call.CommentedFeb 24, 2019 at 20:29
  • When you say "callingb(x + 1) does not callb" where are you calling it from? If it is outside ofa it doesn't get called because it is out of scopeCommentedFeb 24, 2019 at 20:29
  • 3
    The function callis executed; the resulting generator is simply ignored.CommentedFeb 24, 2019 at 20:33
  • 1
    Not sure ifprint("calling b.") is confusing you? And since you have noyield from your recursive call - that will also not return what you probably expect it to.. as snake points out.CommentedFeb 24, 2019 at 20:34

2 Answers2

5

Theb(x + 1) is called, but not executed until yielded in the context of the calling function.

Usingyield from to yield all the values produced by that call tob() 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 xfor x in a():    print(x)
roschach's user avatar
roschach
9,63617 gold badges92 silver badges141 bronze badges
answeredFeb 24, 2019 at 20:29
jordiburgos's user avatar
Sign up to request clarification or add additional context in comments.

4 Comments

I don't understand if b(x + 1) is called (without theyield from), then why "entering b." is not printed?
@handras because generators are lazy. It has been created but its code does not run until you attempt to get it'snext element.
b(x+1) doesn't do anything except return a generator; you still have toiterate over that generator in order for the body to execute. That's the difference betweenyield andreturn.
@handras because that ishow generators are supposed to work. It stops and re-starts exection of the body at yield statements, whenever anext is called. Often in the context of being iterated over.
3

Theanswer 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 xfor x in a():    print(x)

Now let's run this in Python 3.x:

entering b.0calling 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 callingb(x + 1), and thatresult is this<generator object ...> thing.

You then have to do somethingwith 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 xfor x in a():    print(x)

Running this produces:

entering b.0calling b.calling b resulted in temp = <generator object a.<locals>.b at 0x800ac9518>entering b.by doing next(temp), I got 0return from b.leaving b.

Theyield from variant in the other answer basically means "keep calling temp and yielding whatever it yields, until it says it's done". Thisy = 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 xfor x in a():    print(x)
answeredFeb 24, 2019 at 22:06
torek's user avatar

Comments

Your Answer

Sign up orlog in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

By clicking “Post Your Answer”, you agree to ourterms of service and acknowledge you have read ourprivacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.