处理生成器中抛出的异常

74

我有一个生成器和一个消费它的函数:

def read():
    while something():
        yield something_else()

def process():
    for item in read():
        do stuff
如果生成器引发异常,我希望在使用方函数中处理该异常,然后继续迭代直至耗尽。请注意,我不想在生成器中加入任何异常处理代码。
我考虑过以下代码:
reader = read()
while True:
    try:
        item = next(reader)
    except StopIteration:
        break
    except Exception as e:
        log error
        continue
    do_stuff(item)

但这看起来对我而言相当尴尬。

4个回答

74

生成器抛出异常后,它会退出。您无法继续使用它生成的项目。

示例:

>>> def f():
...     yield 1
...     raise Exception
...     yield 2
... 
>>> g = f()
>>> next(g)
1
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in f
Exception
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

如果您控制生成器代码,可以在生成器内部处理异常;否则,应尽量避免发生异常。


1
谢谢!看起来是这样。你能看一下后续问题吗:https://dev59.com/XWgu5IYBdhLWcg3wYWFX? - georg
@Sven,你并不总是能够控制异常的抛出 - 比如来自于一个DB SDK游标流(生成器),而该生成器代码也是专有的,因此无法获得。在这种情况下是否有解决方法?(例如存储流状态并跳过问题记录?) - alancalvitti
@alancalvitti 不,一旦生成器抛出异常,状态就消失了。可能有一些不可移植的黑客方式来检查点它,但你甚至不应该考虑使用。针对那段代码提交一个错误报告,并查看它们是否有任何可以替代的低级接口。 - Sven Marnach

25

这也是我不确定是否正确/优雅处理的事情。

我的做法是从生成器中yield一个Exception,然后在其他地方抛出它。例如:

class myException(Exception):
    def __init__(self, ...)
    ...

def g():
    ...
    if everything_is_ok:
        yield result
    else:
        yield myException(...)

my_gen = g()
while True:
    try:
        n = next(my_gen)
        if isinstance(n, myException):
            raise n
    except StopIteration:
        break
    except myException as e:
        # Deal with exception, log, print, continue, break etc
    else:
        # Consume n

这样我仍然可以传递异常而不会引发它,这将导致生成器函数停止。主要缺点是我需要在每次迭代时使用isinstance检查产生的结果。我不喜欢能够产生不同类型结果的生成器,但作为最后的选择可以使用。


2
谢谢,这与我最终所做的相似(请参见此答案)。 - georg
感谢@georg指出这个答案。我认为,使用Exception生成一个tuple是更好的解决方案。 - dojuba

13

我曾多次需要解决这个问题,在搜索了其他人的解决方案后找到了这个问题。


使用throw而不是raise

一种选择是稍微重构一下代码,将异常在生成器中 throw 给另一个错误处理生成器而不是 raise。下面是可能的实现方式:

def read(handler):
    # the handler argument fixes errors/problems separately
    while something():
        try:
            yield something_else()
        except Exception as e:
            handler.throw(e)
    handler.close()

def err_handler():
    # a generator for processing errors
    while True:
        try:
            yield
        except Exception1:
            handle_exc1()
        except Exception2:
            handle_exc2()
        except Exception3:
            handle_exc3()
        except Exception:
            raise

def process():
    handler = err_handler()
    handler.send(None)  # initialize error handler
    for item in read(handler):
        do stuff

这并不总是最佳解决方案,但肯定是一种选择。


通用解决方案

您可以使用装饰器使其更加美观:

class MyError(Exception):
    pass

def handled(handler):
    """
    A decorator that applies error handling to a generator.

    The handler argument received errors to be handled.

    Example usage:

    @handled(err_handler())
    def gen_function():
        yield the_things()
    """
    def handled_inner(gen_f):
        def wrapper(*args, **kwargs):
            g = gen_f(*args, **kwargs)
            while True:
                try:
                    g_next = next(g)
                except StopIteration:
                    break
                if isinstance(g_next, Exception):
                    handler.throw(g_next)
                else:
                    yield g_next
        return wrapper
    handler.send(None)  # initialize handler
    return handled_inner

def my_err_handler():
    while True:
        try:
            yield
        except MyError:
            print("error  handled")
        # all other errors will bubble up here

@handled(my_err_handler())
def read():
    i = 0
    while i<10:
        try:
            yield i
            i += 1
            if i == 3:
                raise MyError()
        except Exception as e:
            # prevent the generator from closing after an Exception
            yield e

def process():
    for item in read():
        print(item)


if __name__=="__main__":
    process()

输出:

0
1
2
error  handled
3
4
5
6
7
8
9

然而,这样做的缺点是您仍然需要在生成器内部放置通用的Exception处理程序,以便处理可能发生的错误。由于在生成器中引发任何异常都会将其关闭,因此无法避免这种情况。


一个想法的核心

如果有一种yield raise语句可以让生成器在错误被引发后继续运行,那就太好了。然后,您可以编写如下代码:

@handled(my_err_handler())
def read():
    i = 0
    while i<10:
        yield i
        i += 1
        if i == 3:
            yield raise MyError()

...而handler()装饰器可能看起来像这样:

def handled(handler):
    def handled_inner(gen_f):
        def wrapper(*args, **kwargs):
            g = gen_f(*args, **kwargs)
            while True:
                try:
                    g_next = next(g)
                except StopIteration:
                    break
                except Exception as e:
                    handler.throw(e)
                else:
                    yield g_next
        return wrapper
    handler.send(None)  # initialize handler
    return handled_inner

你的装饰器方法行不通。生成器在异常被引发后退出。否则,使用错误处理生成器的想法很好。 - C. Yduqoli
@C.Yduqoli 是的,我有一段时间没有看过这个了,但你关于装饰器的想法可能是正确的。 - Rick

3
自Python 3.3以后,从原始生成器中捕获异常的代码将变得非常简单:
from types import GeneratorType


def gen_decorator(func):
    def gen_wrapper(generator):
        try:
            yield from generator  # I mean this line!
        except Exception:
            print('catched in gen_decorator while iterating!'.upper())
            raise

    def wrapper():
        try:
            result = func()

            if isinstance(result, GeneratorType):
                result = gen_wrapper(result)

            return result
        except Exception:
            print('catched in gen_decorator while initialization!'.upper())
            raise

    return wrapper

一个使用示例:

@gen_decorator
def gen():
    x = 0
    while True:
        x += 1

        if x == 5:
            raise RuntimeError('error!')

        yield x


if __name__ == '__main__':
    try:
        for i in gen():
            print(i)

            if i >= 10:
                print('lets stop!')
                break
    except Exception:
        print('catched in main!'.upper())
        raise

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