Как я могу использовать GeneratorExit? - PullRequest
0 голосов
/ 05 июня 2018

У меня есть следующее mcve :

import logging
class MyGenIt(object):
    def __init__(self, name, content):
        self.name = name
        self.content = content
    def __iter__(self):
        with self:
            for o in self.content:
                yield o
    def __enter__(self):
        return self
    def __exit__(self,  exc_type, exc_value, traceback):
        if exc_type:
            logging.error("Aborted %s", self,
                          exc_info=(exc_type, exc_value, traceback))

А вот пример использования:

for x in MyGenIt("foo",range(10)):
    if x == 5:
        raise ValueError("got 5")

Я хотел бы logging.error сообщить ValueError, но вместо этого он сообщает GeneratorExit:

ERROR:root:Aborted <__main__.MyGenIt object at 0x10ca8e350>
Traceback (most recent call last):
  File "<stdin>", line 8, in __iter__
GeneratorExit

Когда я ловлю GeneratorExit в __iter__:

def __iter__(self):
    with self:
        try:
            for o in self.content:
                yield o
        except GeneratorExit:
            return

ничего не регистрируется (конечно), потому что __exit__вызывается с exc_type=None.

  1. Почему я вижу GeneratorExit вместо ValueError в __exit__?
  2. Что мне делать, чтобы получить желаемое поведение, т.е. ValueError в __exit__?

Ответы [ 3 ]

0 голосов
/ 05 июня 2018

Основная проблема заключается в том, что вы пытаетесь использовать оператор with внутри генератора, чтобы перехватить исключение, которое возникает вне генератора.Вы не можете получить __iter__, чтобы увидеть ValueError, потому что __iter__ не выполняется во время вызова ValueError.

Исключение GeneratorExit вызывается, когда сам генератор удаляется, что происходит, когда он является мусором.собраны.Как только возникает исключение, цикл for завершается;поскольку единственная ссылка на генератор (объект, полученный с помощью вызова __iter__) находится в выражении цикла, завершение цикла удаляет единственную ссылку на итератор и делает ее доступной для сборки мусора.Похоже, что здесь происходит немедленная сборка мусора, а это означает, что исключение GeneratorExit происходит между возвратом ValueError и распространением этого ValueError во вмещающий код.GeneratorExit обычно обрабатывается полностью внутренне;вы видите его только потому, что ваш оператор with находится внутри самого генератора.

Другими словами, поток выглядит примерно так:

  1. Исключение возникает вне генератора
  2. for выходы из цикла
    1. Генератор теперь доступен для сборки мусора
    2. Генератор является сборщиком мусора
      1. Генератор .close() называется
      2. GeneratorExit вызывается внутри генератора
  3. ValueError распространяется на вызывающий код

Последний шаг не происходит до после ваш менеджер контекста видел GeneratorExit.Когда я запускаю ваш код, я вижу ValueError, поднятую после , когда печатается сообщение журнала.

Вы можете видеть, что сборка мусора работает, потому что если вы создадите другую ссылку на итераторСам по себе он будет поддерживать итератор, поэтому он не будет собирать мусор, и поэтому GeneratorExit не произойдет.То есть это «работает»:

it = iter(MyGenIt("foo",range(10)))
for x in it:
    if x == 5:
        raise ValueError("got 5")

В результате ValueError распространяется и становится видимым;GeneratorExit не происходит и ничего не регистрируется.Вы, кажется, думаете, что GeneratorExit каким-то образом «маскирует» вашу ValueError, но на самом деле это не так;это просто артефакт, представленный тем, что не сохранил никаких других ссылок на итератор.Тот факт, что GeneratorExit происходит немедленно в вашем примере, даже не гарантирует поведения;возможно, что итератор не будет собирать мусор до какого-то неизвестного времени в будущем, и в это время будет зарегистрирован журнал GeneratorExit.

Обращаясь к большему вопросу о том, «почему я вижу GeneratorExit»,Ответ заключается в том, что это единственное исключение, которое на самом деле происходит внутри функции генератора.Ошибка ValueError происходит вне генератора, поэтому генератор не может его перехватить.Это означает, что ваш код на самом деле не может работать так, как вы этого хотите.Ваш with оператор внутри функции генератора.Таким образом, он может отлавливать только исключения, возникающие в процессе выдачи предметов из генератора;там генератор не знает, что происходит между моментами, когда он продвигается.Но ваш ValueError возникает в теле цикла над содержимым генератора.Генератор не выполняется в это время;он просто приостановлен.

Вы не можете использовать оператор with в генераторе для магического перехвата исключений, возникающих в коде, который перебирает генератор.Генератор не «знает» о коде, который перебирает его, и не может обрабатывать возникающие там исключения.Если вы хотите перехватывать исключения в теле цикла, вам нужен отдельный оператор with, содержащий сам цикл.

0 голосов
/ 05 июня 2018

Просто быстрое замечание, что вы можете «вывести менеджер контекста» из генератора, и только изменив 3 строки, получите:

import logging
class MyGenIt(object):
    def __init__(self, name, content):
        self.name = name
        self.content = content

    def __iter__(self):
        for o in self.content:
            yield o

    def __enter__(self):
        return self

    def __exit__(self,  exc_type, exc_value, traceback):
        if exc_type:
            logging.error("Aborted %s", self,
                          exc_info=(exc_type, exc_value, traceback))


with MyGenIt("foo", range(10)) as gen:
    for x in gen:
        if x == 5:
            raise ValueError("got 5")

Менеджер контекста, который также может выступать в качестве итератора -и будет перехватывать исключения кода вызывающей стороны, такие как ваш ValueError.

0 голосов
/ 05 июня 2018

GeneratorExit повышается каждый раз, когда закрывается генератор или сопрограмма.Даже без менеджера контекста мы можем воспроизвести точное условие с помощью простой функции генератора, которая выводит информацию об исключительной ситуации при возникновении ошибок (дальнейшее сокращение предоставленного кода, чтобы точно показать, как и где генерируется это исключение).

import sys
def dummy_gen():
    for idx in range(5): 
        try:
            yield idx 
        except:
            print(sys.exc_info())
            raise

for i in dummy_gen():
    raise ValueError('foo')

Использование:

(<class 'GeneratorExit'>, GeneratorExit(), <traceback object at 0x7f96b26b4cc8>)
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ValueError: foo

Обратите внимание, что было также исключение, которое было вызвано внутри самого генератора, поскольку отмечалось, что был выполнен блок except.Обратите внимание, что исключение было также далее raise 'd после оператора print, но обратите внимание, что это на самом деле нигде не показано, потому что оно обрабатывается внутри.

Мы также можем злоупотребить этим фактом, чтобы увидеть, можем ли мыуправляйте потоком, проглотив исключение GeneratorExit и посмотрите, что произойдет.Это можно сделать, удалив оператор raise внутри функции dummy_gen, чтобы получить следующий вывод:

(<class 'GeneratorExit'>, GeneratorExit(), <traceback object at 0x7fd1f0438dc8>)
Exception ignored in: <generator object dummy_gen at 0x7fd1f0436518>
RuntimeError: generator ignored GeneratorExit
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ValueError: foo

Обратите внимание, что был вызван внутренний RuntimeError, который жаловался на игнорирование генераторафункция GeneratorExit.Таким образом, из этого мы можем ясно видеть, что это исключение создается самим генератором внутри функции генератора, и ValueError, который поднимается за пределами , что никогда не присутствует в области видимости внутри .функция генератора.

Поскольку диспетчер контекста будет перехватывать все исключения как есть, а менеджер контекста внутри функции генератора, все возникающие внутри него исключения будут просто передаваться в __exit__ какявляется.Рассмотрим следующее:

class Context(object):
    def __enter__(self):
        return self
    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type:
            logging.error("Aborted %s", self,
                          exc_info=(exc_type, exc_value, traceback))

Измените dummy_gen на следующее:

def dummy_gen():
    with Context():
        for idx in range(5):
            try:
                yield idx
            except:
                print(sys.exc_info())
                raise

Выполните полученный код:

(<class 'GeneratorExit'>, GeneratorExit(), <traceback object at 0x7f44b8fb8908>)
ERROR:root:Aborted <__main__.Context object at 0x7f44b9032d30>
Traceback (most recent call last):
  File "foo.py", line 26, in dummy_gen
    yield idx
GeneratorExit
Traceback (most recent call last):
  File "foo.py", line 41, in <module>
    raise ValueError('foo')
ValueError: foo

То же самое GeneratorExitто, что поднято, теперь представляется диспетчеру контекста, потому что это поведение, которое было определено.

...