Подтвердить что ты не робот

Обрабатывать исключение, созданное генератором

У меня есть генератор и функция, которая его потребляет:

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)

но это выглядит довольно неудобно для меня.

4b9b3361

Ответ 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