try-finally in Python 3 generator

Question:

I have met a snippet of Python 3 code:

def gen():
    try:
        while True:
            yield 1
    finally:
        print("stop")

print(next(gen()))

After I run it, I thought at first that the output should be:

1

But actually the result is:

stop
1

How can this happen? What happened under the hood?

If I run for i in gen(): print(i), there will be an infinite loop which is what I expected. What is the difference between for and next here?

Asked By: ruanhao

||

Answers:

The finally clause is being executed on garbage collection of the generator object.

Consider the following two scenarios:

def gen():
    try:
        while True:
            yield 1
    finally:
        print("stop")

g1 = gen(); print('first time')
print(next(g1))
g2 = gen(); print('second time')  # no stop will be printed because we haven't hit the finally clause yet
def gen():
    try:
        while True:
            yield 1
    finally:
        print("stop")

g = gen(); print('first time')
print(next(g))
g = gen(); print('second time')   # stop will be printed when the first object g was assigned to is garbage collected
Answered By: Vince W.

The loop terminates when the generator is closed, which happens automatically if you don’t save a reference to it. Once that happens, the try statement guarantees that the finally block is executed before the generator object is garbage collected. Compare:

>>> next(gen())
stop
1

with

>>> x = gen()
>>> next(x)
1
Answered By: chepner

I’m answering an old question for future readers.

The finally clause in a generator will be executed either when the interpreter reaches it while executing code inside the generator, or when the generator is finalized(deleted) if the interpreter didn’t reached it while executing code inside the generator. You can read the reference.(See the paragraph starting with "Yield expressions are allowed anywhere".)

The following example demonstrates this.

def gen(name):
    try:
        yield None
        print('after yield', name)
    finally:
        print('after finally', name)

g1 = gen('g1')
next(g1)
next(g1, None)
print('before del g1')
del g1

g2 = gen('g2')
next(g2)
print('before del g2')
del g2

This will output the following.

after yield g1
after finally g1
before del g1
before del g2
after finally g2

For the second question on the for loop, I’ll give an example.

for e in gen():
    print(e)

The above code is equivalent to the following code.

iterator = gen()
while True:
    try:
        e = next(iterator)
    except StopIteration:
        break
    print(e)
del iterator
Answered By: relent95