在Python中捕获生成器调用者抛出的异常

6

我正在尝试捕捉一个生成器调用者抛出的异常:

class MyException(Exception):
    pass

def gen():
    for i in range(3):
        try:
            yield i
        except MyException:
            print 'handled exception'

for i in gen():
    print i
    raise MyException

这将输出

$ python x.py
0
Traceback (most recent call last):
  File "x.py", line 14, in <module>
    raise MyException
__main__.MyException

当我打算输出时
$ python x.py
0
handled exception
1
handled exception
2
handled exception

回顾过去,我认为这是因为调用者与生成器具有不同的堆栈,因此异常没有向上冒泡到生成器。这正确吗?还有其他方法可以捕获调用者引发的异常吗?

另外:我可以使用generator.throw()使其工作,但这需要修改调用者:

def gen():
    for i in range(3):
        try:
            yield i
        except MyException:
            print 'handled exception'
            yield

import sys
g = gen()
for i in g:
    try:
        print i
        raise MyException
    except:
        g.throw(*sys.exc_info())

1
使用 yield <变量名> 返回值永远不会引发异常。可以将生成器视为在返回值之间“暂停”。如果执行 a = gen(); next(a); raise MyException,那么异常会如何处理? - Artyer
那是不可能的。生成器没有任何参与的方式。 - user2357112
2个回答

8
你可能会认为,当执行到生成器中的 yield 时,生成器会执行 for 循环体,就像带有 yield 和块的 Ruby 函数一样。但在 Python 中并不是这样运作的。
当执行到 yield 时,生成器的堆栈帧被暂停并从堆栈中移除,控制权返回到(隐式)调用生成器的 next 方法的代码。然后该代码进入循环体。在引发异常时,生成器的堆栈帧不在堆栈上,并且异常不会通过生成器而向上冒泡。
生成器无法响应此异常。

1
谢谢。当我写那段代码时,我确实使用了 Ruby 的 yield/blocks,这是一个(不正确的)心理模型。 - Snowball

2
你可能也和我刚才到这里时一样感到困惑,对于上下文管理器(contextlib.contextmanager)中的yield不知所措。 它们的用法可以是:
from contextlib import contextmanager


@contextmanager
def mycontext():
    try:
        yield
    except MyException:
        print 'handled exception'

所以,针对你上面描述的类似情况,我的解决方案是:

def gen():
    for i in range(3):
        yield i


for ii in gen():
    with mycontext():
        print ii
        raise MyException

这将产生预期的输出并使用所有的yield。

有点晚了,但是也许有类似疑惑的人会觉得有帮助。

注意:将上下文放在生成器内部将是与在那里使用try ... except一样的错误!!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接