Обрабатывать исключение, созданное генератором
У меня есть генератор и функция, которая его потребляет:
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)
но это выглядит довольно неудобно для меня.
Ответы
Ответ 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
Если вы управляете кодом генератора, вы можете обрабатывать исключение внутри генератора; если нет, вы должны попытаться избежать возникновения исключения.
Ответ 2
Это тоже то, что я не уверен, правильно ли я/элегантно обрабатываю.
То, что я делаю, это yield
a 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
Таким образом, я все еще переношу Exception, не поднимая его, что вызвало бы остановку функции генератора. Главный недостаток заключается в том, что мне нужно проверить полученный результат с помощью isinstance
на каждой итерации. Мне не нравится генератор, который может давать результаты разных типов, но использовать его в качестве последнего средства.
Ответ 3
Я должен был решить эту проблему пару раз и столкнулся с этим вопросом после поиска того, что сделали другие люди.
Бросай вместо рейза
Один option-, который потребует небольшого рефакторинга bit-, - это 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
Ответ 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