Ответ 1
Вы, как и ожидалось, полагаетесь на специфичное для конкретной реализации поведение подсчета ссылок CPython. 1
Фактически, если вы запустите этот код, скажем, в PyPy, выход будет обычно:
Counting... 10
Counting... 9
Counting... 8
Counting... 7
Counting... 6
Finished counting
In the finally block
И если вы запустите его в интерактивном сеансе PyPy, эта последняя строка может наступить много строк позже или даже когда вы окончательно выйдете.
Если вы посмотрите, как работают генераторы, у них есть методы примерно так:
def __del__(self):
self.close()
def close(self):
try:
self.raise(GeneratorExit)
except GeneratorExit:
pass
CPython немедленно удаляет объекты, когда счетчик ссылок становится равным нулю (у него также есть сборщик мусора, чтобы разбить циклические ссылки, но здесь это не актуально). Как только генератор выходит из области видимости, он удаляется, поэтому он закрывается, поэтому он генерирует GeneratorExit
в рамку генератора и возобновляет его. И, конечно же, нет никакого обработчика для GeneratorExit
, поэтому предложение finally
выполняется, и управление переходит в стек, где исключение проглатывается.
В PyPy, который использует гибридный сборщик мусора, генератор не удаляется, пока в следующий раз GC не решит сканировать. И в интерактивной сессии, при низком давлении в памяти, это может быть уже до выхода. Но как только это происходит, происходит то же самое.
Вы можете это увидеть, явно обработав GeneratorExit
:
def countdown(n):
try:
while n > 0:
yield n
n -= 1
except GeneratorExit:
print('Exit!')
raise
finally:
print('In the finally block')
(Если вы оставите raise
, вы получите те же результаты только по нескольким причинам).
Вы можете явно close
генератор - и, в отличие от вышеизложенного, это часть открытого интерфейса типа генератора:
def main():
c = countdown(10)
for n in c:
if n == 5:
break
print('Counting... ', n)
c.close()
print('Finished counting')
Или, конечно, вы можете использовать оператор with
:
def main():
with contextlib.closing(countdown(10)) as c:
for n in c:
if n == 5:
break
print('Counting... ', n)
print('Finished counting')
1. Как указывает Tim Peters, вы также полагаетесь на специфику реализации CPython-компилятора во втором тесте.