【问题标题】:How do I use GeneratorExit?如何使用 GeneratorExit?
【发布时间】:2018-11-14 10:34:09
【问题描述】:

我有以下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__ 中捕获GeneratorExit 时:

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

没有记录(当然)因为__exit__ 是用exc_type=None 调用的。

  1. 为什么我在__exit__ 中看到的是GeneratorExit 而不是ValueError
  2. 我该怎么做才能获得所需的行为,即ValueError in __exit__

【问题讨论】:

  • 所提供代码的具体目标是什么?您是否试图提供一个也是迭代器的上下文管理器?您为什么要尝试将实例本身用作上下文管理器来获取迭代器?如果您只进行迭代,则不需要在 __iter__ 内调用 with self:,因为根据类的定义,它已经在其中。
  • @metatoaster:这是一个最小的例子——实际上__enter____exit__都做了一些重要的事情。
  • 好的,要正确地做到这一点,你真的想把这两个东西(上下文管理器和迭代器)分成两个独立的东西,因为上下文管理器也是一个可以使事情混乱的生成器。但是,如果您能够指定如何使用此类,则可以做一些事情。
  • @metatoaster:实际上,为了简洁起见,我将两个类(context 和 iter)合并为一个。关键是我不明白为什么我看到的是GeneratorExit 而不是ValueError
  • 虽然您似乎可以通过检查__exit__ 中的当前堆栈来找到违规行,但this related 可能会有所帮助。

标签: python python-3.x iterator generator


【解决方案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'd,但请注意它实际上并未显示在任何地方,因为它是在内部处理的。

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

(<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 函数。所以我们从这里我们可以清楚地看到这个异常是由生成器本身在生成器函数内部产生的,而在 outside 引发的 ValueError 范围永远不会出现在 inside 生成器函数。

由于上下文管理器将按原样捕获所有异常,并且上下文管理器生成器函数中,因此在其中引发的任何异常都将按原样传递给__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 现在呈现给上下文管理器,因为这是已定义的行为。

【讨论】:

  • 这是正确的,但请注意,这里也发生了 GeneratorExit,因为您没有保留对迭代器的引用。如果您这样做,将不会发生 GeneratorExit(直到您删除您保留的任何额外引用)。
  • @BrenBarn 是的,我在第一句话中省略了关于“发电机关闭”是什么意思的解释,因为我分散了自己的注意力,构建了显示 OP 的确切位置的示例。
【解决方案2】:

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

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

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

  1. 在生成器之外引发异常
  2. for 循环退出
    1. 生成器现在可用于垃圾收集
    2. 生成器被垃圾回收
      1. 生成器的.close() 被调用
      2. GeneratorExit 在生成器内部引发
  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”这个更大的问题,答案是这是生成器函数中实际发生的唯一异常。 ValueError 发生在生成器之外,因此生成器无法捕获它。这意味着您的代码不能真正按照您想要的方式工作。您的with 语句在生成器函数内部。因此它只能捕获生成器生成项目过程中发生的异常;那里的发电机不知道它前进的时间之间会发生什么。但是您的 ValueError 是在生成器内容的循环体中引发的。此时生成器没有执行;它只是坐在那里暂停。

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

【讨论】:

    【解决方案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。

    【讨论】:

    • 是的,我也打算写这个,但鉴于 OP 希望在没有生成器用户担心的情况下完成特定的清理工作,我最终也没有把它放在我的答案中,因为它更改预期的 API。也就是说,可以实现__iter__ 来检查__enter__ 可能设置的标志,以确保在同一实例提供的上下文管理器中执行迭代。
    • 很难说 OP 的目标是什么,但这似乎是最接近的。就像你说的,可以使用相同的 object 作为迭代器和上下文管理器。但是 withfor 语句的实际语法都需要在使用时明确,如您的示例中所示;您不能在迭代器内部“隐藏”with 语句。
    猜你喜欢
    • 1970-01-01
    • 2016-10-27
    • 1970-01-01
    • 2020-09-26
    • 1970-01-01
    • 2018-03-14
    • 1970-01-01
    • 1970-01-01
    • 2018-07-29
    相关资源
    最近更新 更多