Bernhard Liebl
Bernhard Liebl

Reputation: 21

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?

Upvotes: 2

Views: 317

Answers (2)

torek
torek

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

jordiburgos
jordiburgos

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

Related Questions