Как вы и ожидали, вы полагаетесь на специфическое для реализации поведение подсчета ссылок 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. Как указывает Тим Питерс , вы также полагаетесь на специфичное для реализации поведение компилятора CPython во втором тесте.