简体   繁体   中英

Confusing python yield behavior

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?

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)

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)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM