Как доработаны контекстные менеджеры в непотребленных генераторах?

Я не понимаю, как и когда контекстный менеджер в недоделанном генераторе закрывается. Рассмотрим следующий менеджер контекста и функцию:

from contextlib import contextmanager

@contextmanager
def ctx():
    print('enter ctx')
    yield
    print('exit ctx')

def gen_nums(n):
    with ctx():
        yield from range(n)

Моя первая интуиция заключалась в том, что если я вызову gen_nums, но не использую генератор полностью, ctx никогда не будет закрыт, что было довольно тревожно. Например:

for i, j in zip(range(5), gen_nums(10)):
    print(f'{i}, {j}')

Здесь exit ctx не печатается в конце. Насколько я понял, это означало, что если бы у меня был контекст файла в генераторе, он оставался бы открытым; однако затем я понял, что то же самое с файлами фактически правильно закроет файл. После некоторых тестов я узнал, что если я это сделаю:

from contextlib import contextmanager

@contextmanager
def ctx():
    print('enter ctx')
    try:
        yield
    finally:
        print('exit ctx')

Теперь в конце было напечатано exit ctx. Поэтому я предполагаю, что в какой-то момент будет вызвано какое-то исключение, но я не знаю, какое, где и когда (я пытался напечатать исключение с помощью except BaseException as e, но это не сработало). Кажется, это происходит при удалении генератора, потому что если я делаю:

g = gen_nums(10)
for i, j in zip(range(5), g):
    print(f'{i}, {j}')
del g

Тогда exit ctx происходит только после del g. Тем не менее, я хотел бы лучше понять, что здесь происходит и кто что запускает.


person jdehesa    schedule 17.01.2019    source источник
comment
Возможный дубликат деструктора Python на основе try/finally + yield?   -  person r.ook    schedule 17.01.2019
comment
Взгляните на помеченную ветку, возможно, она не является полной копией, но кажется релевантной вашему запросу. А именно, когда выполняется del g, я подозреваю, что __del__ в некоторой степени эквивалентен g.close(), который завершает работу генератора без StopIteration, чтобы разрешить освобождение ресурсов в диспетчере контекста. Следовательно, поскольку Генератор не выдает ошибки, ctx нечего ловить в try... except. Однако если вы сделали g.throw(SomeError) во время его существования, вы увидите ctx эту ошибку.   -  person r.ook    schedule 17.01.2019
comment
Однако я не могу полностью воспроизвести класс фиктивного генератора, чтобы раскрыть внутреннюю работу, чтобы подтвердить свои подозрения, поэтому я не могу полностью дать ответ. Общий поток, который я понимаю здесь, таков: g создается, ctx.__enter__() запускается, g.send() и т. д. во время работы генератора, а затем происходит g.close() или эквивалентный, и возвращается к ctx.__exit__() (который принимает любую ошибку, которая была выдана).   -  person r.ook    schedule 17.01.2019
comment
@Idlehands Спасибо за ссылку. Это не совсем решает вопрос, но у него есть указатели для его решения. Что происходит, так это то, что, как вы предлагаете, удаление незавершенного генератора вызовет его метод stop, и это вызовет исключение GeneratorExit, которое будет передано диспетчеру контекста. На самом деле, если я заменю finally: во второй версии ctx на except GeneratorExit:, это тоже сработает (ранее я безуспешно пытался напечатать возбужденное исключение, потому что печать GeneratorExit ничего не показывает).   -  person jdehesa    schedule 18.01.2019
comment
@Idlehands Я думаю, что можно оставить вопрос, потому что он на самом деле не такой, как другой, но, возможно, вам следует опубликовать полный ответ, поскольку вы действительно узнали об этом, если вы согласны.   -  person jdehesa    schedule 18.01.2019
comment
О, подождите, это может быть неправильно на самом деле. Если вы сделаете c = ctx(); c.__enter__(); del c, вы получите исключение GeneratorExit. На самом деле оба генератора @contextmanager вызывают GeneratorExit, когда они удаляются перед завершением, я просто не уверен, какой из них вызывается первым.   -  person jdehesa    schedule 18.01.2019
comment
Как ни странно, я даже не могу сделать c.__enter__(), он жалуется на меня TypeError: '_GeneratorContextManager' object is not an iterator. Я хотел получить некоторую ясность между тем, какой из ctx или g поднимает GeneratorExit yb, добавив некоторое описание, но я просто недостаточно хорошо разбираюсь, чтобы зайти так далеко. Возможно, придется создать собственный Ctx с __enter__ __exit__ методами. Я не решаюсь дать наполовину ответ, который я сам не совсем понимаю, поэтому я оставил его в качестве комментария в надежде, что другие прояснят ваше решение.   -  person r.ook    schedule 18.01.2019


Ответы (1)


Учти это:

@contextmanager
def ctx():
    print('enter ctx')
    try:
        print('ctx begins yield')
        yield
        print('ctx finishes yield')
    finally:    
        print('exit ctx')

def gen_nums(n):
    print('entering generator')
    l = range(n)
    with ctx():
        print('inside context manager')
        for i in l:
            print('gen before yield')
            yield i
            print('gen after yield')
        print('existing ctx')
    print('exiting generator')

Результат такой:

>>> g = gen_nums(3)
>>> next(g)
entering generator
enter ctx
ctx begins yield
inside context manager
gen before yield
0
>>> next(g)
gen after yield
gen before yield
1
>>> next(g)
gen after yield
gen before yield
2
>>> next(g)
gen after yield
exiting ctx
ctx finishes yield
exit ctx
exiting generator
Traceback (most recent call last):
  File "<pyshell#165>", line 1, in <module>
    next(g)
StopIteration

Похоже, что contextmanager по-настоящему завершается только в точке после последней итерации generator и до того, как она достигнет StopIteration. Таким образом, на самой последней итерации generator contextmanager останется открытым, если он не будет запущен в цикле for (который обрабатывает StopIteration).

Вы также не сможете поймать StopIteration внутри contextmanager, потому что это происходит после исчерпания generator, после чего contextmanager уже был бы завершен.

Кроме того, IIRC, при построении contextmanager вы всегда должны использовать try... finally... в любом случае.

person r.ook    schedule 17.01.2019
comment
Я знаю, что контекст завершается нормально, если генератор достигает StopIteration. Мой вопрос касается случая, когда этого не происходит. Например, g = gen_nums(3); next(g); del g все равно напечатает exit ctx, и я хочу знать, как это работает. finally действительно часто используется в контекстных менеджерах, хотя я использовал его в случае возникновения исключения в блоке with; на что я указываю, что это важно и с генераторами, даже если исключения не возбуждаются (по крайней мере, явно). - person jdehesa; 17.01.2019