Как завершаются контекстные менеджеры в неиспользованных генераторах? - PullRequest
0 голосов
/ 17 января 2019

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

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. Однако я хотел бы лучше понять, что здесь происходит и кто что запускает.

1 Ответ

0 голосов
/ 17 января 2019

Учтите это:

@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... в любом случае.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...