ジェネレーターとそれを使用する関数があります。
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)
しかし、これはかなり厄介に見えます。
ジェネレーターが例外をスローすると、終了します。生成されたアイテムを消費し続けることはできません。
例:
>>> 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
ジェネレーターコードを制御する場合、ジェネレーター内で例外を処理できます。そうでない場合は、例外が発生しないようにする必要があります。
これは、正しく/エレガントに処理するかどうかもわかりません。
私がやるのは、ジェネレーターからの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
で得られた結果を確認する必要があることです。さまざまなタイプの結果を生成できるジェネレーターは好きではありませんが、最後の手段として使用します。
私はこの問題を数回解決する必要があり、他の人が何をしたかを検索した後にこの質問に出くわしました。
少しリファクタリングを必要とするオプションの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
_