web-dev-qa-db-ja.com

ジェネレーターでスローされた例外を処理する

ジェネレーターとそれを使用する関数があります。

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)

しかし、これはかなり厄介に見えます。

44
georg

ジェネレーターが例外をスローすると、終了します。生成されたアイテムを消費し続けることはできません。

例:

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

ジェネレーターコードを制御する場合、ジェネレーター内で例外を処理できます。そうでない場合は、例外が発生しないようにする必要があります。

49
Sven Marnach

これは、正しく/エレガントに処理するかどうかもわかりません。

私がやるのは、ジェネレーターからの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で得られた結果を確認する必要があることです。さまざまなタイプの結果を生成できるジェネレーターは好きではありませんが、最後の手段として使用します。

7
dojuba

私はこの問題を数回解決する必要があり、他の人が何をしたかを検索した後にこの質問に出くわしました。


上げるのではなく投げる

少しリファクタリングを必要とするオプションの1つは、throw itではなく、ジェネレーターの例外を別のエラー処理ジェネレーターに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
_
4
Rick Teachey