Exit generator in Python generator

I wrote a test program about a Python generator. But I got an error that is not expected. And I don't know how to explain it. Let me show you the code:

def countdown(n):
    logging.debug("Counting down")
    while n > 0:
        try:
            yield n
        except GeneratorExit:
            logging.error("GeneratorExit")
        n -= 1


if __name__ == '__main__':
    c = countdown(10)
    logging.debug("value: %d", c.next())

      

I thought it should work without issue. But the conclusion is:

# ./test.py
[2015/06/16 04:10:49] DEBUG    - Counting down     
[2015/06/16 04:10:49] DEBUG    - value: 10 
[2015/06/16 04:10:49] ERROR    - GeneratorExit
Exception RuntimeError: 'generator ignored GeneratorExit' in <generator object countdown at 0x7f9934407640> ignored

      

Why is there an error in the last line. I don't know why I threw the GeneratorExit exception. Is there something generator I have missed? I also typed the code into an interactive python shell and everything is fine. How can this happen?

+6


source to share


3 answers


Suppose you have the following generator:

def gen():
    with open('important_file') as f:
        for line in f:
            yield line

      

and you next

- once and throw it away:



g = gen()
next(g)
del g

      

The generator control thread never leaves the block with

, so the file is not closed. To prevent this, when the generator is garbage collected, Python calls its method close

, which raises an exception GeneratorExit

at the point from which the last generator was yield

edited. This exception is intended to run any blocks finally

or context manager __exit__

that did not get the ability to run.

When you catch GeneratorExit

and keep moving, Python sees that the generator is not out of order. Since this could indicate that the resources were not properly released, Python reports this as a RuntimeError.

+18


source


When a generator object is garbage collected at the end of your program, its method close()

is called and this throws an exception GeneratorExit

inside the generator. This is usually not caught and results in a generator output.



Since you catch the exception and move to a different value, this is causing RuntimeError

. If you catch an exception GeneratorExit

, you need to either re-raise or exit the function without bringing anything else.

+6


source


Perhaps this is because your exit value n is inside try blocks that always return a new reference n, which makes the last value of n automatically garbage collected. The PEP 342 also states :

"Add support to ensure that close() is called when a generator iterator is garbage-collected"
"Allow yield to be used in try/finally blocks, since garbage collection or an explicit close() call would now allow the finally clause to execute."

      

Since the close method in the generator is equivalent to logging.error('GeneratorExit')

GeneratorExit and is caught by your exception, logging.error('GeneratorExit')

expression logging.error('GeneratorExit')

.

The "RunTimeError" is raised because the generator returns the following value n (9), as stated in the python documentation https://docs.python.org/3.6/reference/expressions.html#generator-iterator-methods :

"Raises a GeneratorExit at the point where the generator function was paused. If the generator function then exits gracefully, is already closed, or raises GeneratorExit (by not catching the exception), close returns to its caller. **If the generator yields a value, a RuntimeError is raised**. If the generator raises any other exception, it is propagated to the caller. close() does nothing if the generator has already exited due to an exception or normal exit"

      

Maybe the code should look like this:

#pygen.py

import sys
import logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s \
        %(levelname)s - %(message)s', datefmt='[%Y/%m/%d %H:%M:%S]')

def genwrapper(func):
    #makes gen wrapper
    #which automatically send(None) to generator
    def wrapper(n=None):
        f = func(n)
        f.send(None)
        return f
    return wrapper

@genwrapper
def countdown(n=None):
    logging.debug('Counting Down')
    while True:
        try:
            n = yield(n)
        except GeneratorExit as e:
            logging.error('GeneratorExit')
            raise e

if __name__ == '__main__':
    n = int(sys.argv[1])
    c = countdown() #avoid function call in loop block (avoid new reference to c)
     while n > 0:
         a = c.send(n)
         logging.debug('Value: %d', a)
         n -= 1

      

then in your terminal:

guest@xxxxpc:~$ python pygen.py 5

      

will result in:

[2018/12/13 16:50:45]         DEBUG - Counting Down
[2018/12/13 16:50:45]         DEBUG - Value: 5
[2018/12/13 16:50:45]         DEBUG - Value: 4
[2018/12/13 16:50:45]         DEBUG - Value: 3
[2018/12/13 16:50:45]         DEBUG - Value: 2
[2018/12/13 16:50:45]         DEBUG - Value: 1
[2018/12/13 16:50:45]         ERROR - GeneratorExit

      

Sorry for my bad english or my suggestion if not clear enough, thanks

0


source







All Articles