web-dev-qa-db-fra.com

Gérer une exception levée dans un générateur

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.

44
georg

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.

49
Sven Marnach

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.

7
dojuba

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.


Lancer au lieu de relever

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.


Solution généralisée

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.


Noyau d'une idée

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
4
Rick Teachey