代码之家  ›  专栏  ›  技术社区  ›  sds Niraj Rajbhandari

如何使用generatorexit?

  •  2
  • sds Niraj Rajbhandari  · 技术社区  · 6 年前

    我有以下的 mcve :

    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
    

    当我赶上 发电机出口 在里面 __iter__ :

    def __iter__(self):
        with self:
            try:
                for o in self.content:
                    yield o
            except GeneratorExit:
                return
    

    当然没有记录因为 __exit__ 是用 exc_type=None .

    1. 为什么我看见 发电机出口 而不是 值误差 在里面 第二代 ?
    2. 我该怎么做才能得到想要的行为,即, 值误差 在里面 第二代 ?
    3 回复  |  直到 6 年前
        1
  •  3
  •   jedwards    6 年前

    简单地说,您可以从生成器中“调出上下文管理器”,只需更改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
  •  3
  •   BrenBarn    6 年前

    最基本的问题是你试图使用 with 语句捕获在生成器外部引发的异常。你不能得到 __iter__ 查看valueerror,因为 γ迭代 在引发valueerror时未执行。

    当生成器本身被删除时,将引发GeneratorExit异常,在对其进行垃圾收集时会发生这种情况。一旦异常发生, for 循环终止;因为对生成器的唯一引用(通过调用 γ迭代 )在循环表达式中,终止循环将删除对迭代器的唯一引用,并使其可用于垃圾回收。似乎它正在被立即垃圾收集,这意味着generatorexit异常发生 之间 valueerror的提升和该valueerror向封闭代码的传播。generatorexit通常完全在内部处理;您之所以看到它,是因为 具有 语句在生成器内部。

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

    1. 在生成器外部引发异常
    2. 对于 回路出口
      1. 生成器现在可用于垃圾收集
      2. 发电机垃圾回收
        1. 发电机的 .close() 被称为
        2. 发电机出口在发电机内部升高
    3. valueerror传播到调用代码

    最后一步直到 之后 您的上下文管理器已经看到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”这个更大的问题时,答案是这是generator函数中实际发生的唯一异常。valueerror发生在生成器外部,因此生成器无法捕获它。这意味着你的代码不能以你想要的方式工作。你的 具有 语句是 里面 发电机的功能。因此,它只能捕获在从生成器生成项的过程中发生的异常;生成器不知道在它前进的时间间隔内发生了什么。但是valueerror在生成器内容上的循环体中引发。生成器此时没有执行;它只是挂在那里。

    具有 在生成器中的语句,以神奇的方式捕获在遍历生成器的代码中发生的异常。生成器不“知道”在其上迭代的代码,并且无法处理在其上发生的异常。如果要在循环体中捕获异常,则需要单独的 具有 包含循环本身的语句。

        3
  •  1
  •   metatoaster    6 年前

    这个 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 布洛克被处决了。注意,例外情况也更进一步 raise “d在print语句之后,但请注意它实际上并没有显示在任何地方,因为它是在内部处理的。

    我们也可以滥用这个事实,看看我们是否可以通过吞咽 发电机出口 例外,看看会发生什么。这可以通过移除 提升 内部声明 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 有人抱怨发电机忽略了 发电机出口 功能。因此,我们可以清楚地看到,这个异常是由生成器本身在生成器函数中产生的,并且 ValueError 那是提高了 外部 这个范围永远不存在 里面 发电机的功能。

    因为上下文管理器将按原样捕获所有异常,而上下文管理器是 里面 生成器函数,它内部引发的任何异常都将简单地传递给 __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))
    

    修改 杜米尔根 以下内容:

    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
    

    相同的 发电机出口 现在将向上下文管理器显示所引发的,因为这是定义的行为。