我如何使用GeneratorExit?

时间:2018-06-05 03:36:51

标签: python python-3.x iterator generator

我有以下mcve

CREATE PUBLIC DATABASE LINK MONITORING_CONFIGURATION
       USING 'MONITORING_CONFIGURATION'; 

以下是样本使用:

import logging
class MyGenIt(object):
    def __init__(self, name, content):
        self.name = name
        self.content = content
    def __iter__(self):
        with self:
            for o in self.content:
                yield o
    def __enter__(self):
        return self
    def __exit__(self,  exc_type, exc_value, traceback):
        if exc_type:
            logging.error("Aborted %s", self,
                          exc_info=(exc_type, exc_value, traceback))

我希望for x in MyGenIt("foo",range(10)): if x == 5: raise ValueError("got 5") 报告logging.error,而是报告ValueError

GeneratorExit

当我在ERROR:root:Aborted <__main__.MyGenIt object at 0x10ca8e350> Traceback (most recent call last): File "<stdin>", line 8, in __iter__ GeneratorExit 中抓住GeneratorExit时:

__iter__

没有记录任何内容(当然),因为def __iter__(self): with self: try: for o in self.content: yield o except GeneratorExit: return 是使用__exit__调用的。

  1. 为什么我在exc_type=None中看到GeneratorExit而不是ValueError
  2. 如何才能获得所需的行为,即__exit__中的ValueError

3 个答案:

答案 0 :(得分:3)

基本问题是您尝试在生成器中使用with语句来捕获在生成器外部引发的异常。您无法让__iter__看到ValueError,因为__iter__在引发ValueError时没有执行。

当生成器本身被删除时会引发GeneratorExit异常,这在垃圾回收时会发生。一旦发生异常,for循环就会终止;由于对生成器的唯一引用(通过调用__iter__获得的对象)位于循环表达式中,因此终止循环将删除对迭代器的唯一引用,并使其可用于垃圾回收。看来这里立即被垃圾收集,这意味着GeneratorExit异常发生在之间引发ValueError和将ValueError传播到封闭代码。 GeneratorExit通常完全在内部处理;你只是看到它,因为你的with语句在发生器本身内。

换句话说,流程是这样的:

  1. 在发电机外部引发异常
  2. for循环退出
    1. 生成器现在可用于垃圾收集
    2. 生成器被垃圾收集
      1. 生成器的.close()称为
      2. GeneratorExit在生成器内引发
  3. ValueError传播到调用代码
  4. 上下文管理器看到GeneratorExit之后,才会发生最后一步。当我运行你的代码时,我看到在打印日志消息后引发了的ValueError。

    你可以看到垃圾收集正在工作,因为如果你创建另一个对迭代器本身的引用,它将使迭代器保持活动状态,因此它不会被垃圾收集,因此不会发生GeneratorExit。也就是说,这“有效”:

    it = iter(MyGenIt("foo",range(10)))
    for x in it:
        if x == 5:
            raise ValueError("got 5")
    

    结果是ValueError传播并可见;没有发生GeneratorExit,也没有记录任何内容。你似乎认为GeneratorExit以某种方式“掩盖”了你的ValueError,但事实并非如此;它只是通过不保留对迭代器的任何其他引用而引入的工件。 GeneratorExit在您的示例中立即发生的事实甚至不能保证行为;迭代器可能在将来某个未知时间之前不会被垃圾收集,然后将记录GeneratorExit。

    关于“我为什么看到GeneratorExit”这个更大的问题,答案是这是生成器函数中实际发生的唯一例外。 ValueError发生在生成器外部,因此生成器无法捕获它。这意味着您的代码无法以您希望的方式运行。您的with语句 in 生成器函数。因此,它只能捕获从生成器中产生项目的过程中发生的异常;生成器不知道它前进的时间之间会发生什么。但是你的ValueError是在生成器内容的循环体中引发的。此时发电机没有执行;它只是坐在那里暂停。

    您不能在生成器中使用with语句来神奇地捕获在迭代生成器的代码中发生的异常。生成器不“知道”迭代它的代码,也不能处理那里发生的异常。如果要捕获循环体内的异常,则需要一个单独的with语句来封闭循环本身。

答案 1 :(得分:3)

只需快速说明您可以“将上下文管理器带出”生成器,并且只需更改3行即可:

import logging
class MyGenIt(object):
    def __init__(self, name, content):
        self.name = name
        self.content = content

    def __iter__(self):
        for o in self.content:
            yield o

    def __enter__(self):
        return self

    def __exit__(self,  exc_type, exc_value, traceback):
        if exc_type:
            logging.error("Aborted %s", self,
                          exc_info=(exc_type, exc_value, traceback))


with MyGenIt("foo", range(10)) as gen:
    for x in gen:
        if x == 5:
            raise ValueError("got 5")

一个也可以充当迭代器的上下文管理器 - 并且会捕获像ValueError一样的调用者代码异常。

答案 2 :(得分:1)

只要生成器或协同程序关闭,就会引发GeneratorExit。即使没有上下文管理器,我们也可以使用简单的生成器函数复制确切的条件,该函数在错误时打印出异常信息(进一步减少提供的代码以准确显示生成异常的方式和位置)。

import sys
def dummy_gen():
    for idx in range(5): 
        try:
            yield idx 
        except:
            print(sys.exc_info())
            raise

for i in dummy_gen():
    raise ValueError('foo')

用法:

(<class 'GeneratorExit'>, GeneratorExit(), <traceback object at 0x7f96b26b4cc8>)
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ValueError: foo

注意,还有一个异常是在内部引发生成器本身,正如所指出的那样except块被执行了。请注意,在print语句之后该异常也是raise',但请注意它实际上并未在任何地方显示,因为它是在内部处理的。

我们也可以滥用这个事实,看看我们是否可以通过吞下GeneratorExit异常来操纵流程,看看会发生什么。这可以通过删除raise函数中的dummy_gen语句来获得以下输出:

(<class 'GeneratorExit'>, GeneratorExit(), <traceback object at 0x7fd1f0438dc8>)
Exception ignored in: <generator object dummy_gen at 0x7fd1f0436518>
RuntimeError: generator ignored GeneratorExit
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ValueError: foo

请注意引发的内部RuntimeError是如何抱怨生成器忽略GeneratorExit函数的。因此,我们可以清楚地看到,这个异常是由生成器本身在生成器函数内生成的,而ValueError之外引发的,该范围从不存在 inside < / em>生成器功能。

由于上下文管理器将按原样捕获所有异常,并且上下文管理器内部生成器函数,因此在其中引发的任何异常都将按原样传递给__exit__。请考虑以下事项:

class Context(object):
    def __enter__(self):
        return self
    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type:
            logging.error("Aborted %s", self,
                          exc_info=(exc_type, exc_value, traceback))

dummy_gen修改为以下内容:

def dummy_gen():
    with Context():
        for idx in range(5):
            try:
                yield idx
            except:
                print(sys.exc_info())
                raise

运行生成的代码:

(<class 'GeneratorExit'>, GeneratorExit(), <traceback object at 0x7f44b8fb8908>)
ERROR:root:Aborted <__main__.Context object at 0x7f44b9032d30>
Traceback (most recent call last):
  File "foo.py", line 26, in dummy_gen
    yield idx
GeneratorExit
Traceback (most recent call last):
  File "foo.py", line 41, in <module>
    raise ValueError('foo')
ValueError: foo

引发的GeneratorExit现在呈现给上下文管理器,因为这是已定义的行为。