J'ai un générateur et une fonction qui le consomme:
def read():
while something():
yield something_else()
def process():
for item in read():
do stuff
Si le générateur lève une exception, je veux traiter cela dans la fonction consommateur et continuer à consommer l'itérateur jusqu'à ce qu'il soit épuisé. Notez que je ne veux pas avoir de code de gestion des exceptions dans le générateur.
J'ai pensé à quelque chose comme:
reader = read()
while True:
try:
item = next(reader)
except StopIteration:
break
except Exception as e:
log error
continue
do_stuff(item)
mais cela me semble plutôt gênant.
Lorsqu'un générateur lève une exception, il se ferme. Vous ne pouvez pas continuer à consommer les éléments qu'il génère.
Exemple:
>>> 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
Si vous contrôlez le code du générateur, vous pouvez gérer l'exception à l'intérieur du générateur; sinon, vous devriez essayer d'éviter qu'une exception ne se produise.
C'est aussi quelque chose que je ne sais pas si je gère correctement/élégamment.
Ce que je fais est de yield
un Exception
du générateur, puis de le monter ailleurs. Comme:
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
De cette façon, je reporte toujours l'exception sans la lever, ce qui aurait entraîné l'arrêt de la fonction du générateur. L'inconvénient majeur est que je dois vérifier le résultat obtenu avec isinstance
à chaque itération. Je n'aime pas un générateur qui peut donner des résultats de différents types, mais je l'utilise en dernier recours.
J'ai dû résoudre ce problème à quelques reprises et suis tombé sur cette question après une recherche de ce que d'autres personnes ont fait.
Une option, qui nécessitera un peu de refactorisation, serait de throw
l'exception dans le générateur (vers un autre générateur de gestion des erreurs) plutôt que raise
. Voici à quoi cela pourrait ressembler:
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
Ce ne sera pas toujours la meilleure solution, mais c'est certainement une option.
Vous pourriez tout rendre un peu plus agréable avec un décorateur:
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()
Production:
0
1
2
error handled
3
4
5
6
7
8
9
Cependant, l'inconvénient est que vous devez toujours mettre une gestion générique Exception
à l'intérieur du générateur qui pourrait produire des erreurs. Il n'est pas possible de contourner cela, car le fait de lever une exception dans un générateur le fermera.
Ce serait bien d'avoir une sorte d'instruction yield raise
, Qui permet au générateur de continuer à fonctionner s'il le peut après que l'erreur a été déclenchée. Ensuite, vous pouvez écrire du code comme ceci:
@handled(my_err_handler())
def read():
i = 0
while i<10:
yield i
i += 1
if i == 3:
yield raise MyError()
... et le décorateur handler()
pourrait ressembler à ceci:
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