【问题标题】:Handle an exception thrown in a generator处理生成器中抛出的异常
【发布时间】:2012-07-07 03:22:14
【问题描述】:

我有一个生成器和一个使用它的函数:

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)

但这对我来说看起来很尴尬。

【问题讨论】:

    标签: python exception generator


    【解决方案1】:

    当生成器抛出异常时,它会退出。你不能继续消费它生成的物品。

    例子:

    >>> 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
    

    如果你控制了生成器代码,你可以在生成器内部处理异常;如果没有,你应该尽量避免发生异常。

    【讨论】:

    • 谢谢!情况似乎如此。能否看一下后续问题:stackoverflow.com/q/11366892/989121
    • @Sven,您不能总是控制引发的异常 - 例如,来自 DB SDK 游标流(生成器),它也是专有的,因此生成器代码不可用。在这种情况下有解决方法吗? (例如存储流状态并跳过问题记录?)
    • @alancalvitti 不,一旦生成器抛出异常,状态就消失了。可能有一些不可移植的黑客以某种方式检查它,但你甚至不应该考虑使用。针对该代码提交一个错误,并查看它们是否有任何您可以使用的较低级别的接口。
    【解决方案2】:

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

    我所做的是从生成器到yieldException,然后将其提升到其他地方。喜欢:

    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 检查产生的结果。我不喜欢可以产生不同类型结果的生成器,但将其用作最后的手段。

    【讨论】:

    • 谢谢,这与我最终所做的类似(请参阅this answer
    • 感谢@georg 指出这个答案。我认为用Exception 生成tuple 是一个更好的解决方案。
    【解决方案3】:

    我需要解决这个问题几次,并在搜索其他人做了什么后发现了这个问题。


    投掷而不是举起

    一个选项 - 需要稍微重构一些东西 - 将生成器中的异常 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 是的,我有一段时间没看过这个了,但你对装饰器的看法可能是对的。
    【解决方案4】:

    在 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
    

    【讨论】:

      猜你喜欢
      • 2013-07-24
      • 1970-01-01
      • 2013-03-23
      • 2011-12-12
      • 2013-11-20
      • 1970-01-01
      • 2011-11-15
      相关资源
      最近更新 更多