python генераторы сборка мусора
Я думаю, что мой вопрос связан с этим, но не совсем похож. Рассмотрим этот код:
def countdown(n):
try:
while n > 0:
yield n
n -= 1
finally:
print('In the finally block')
def main():
for n in countdown(10):
if n == 5:
break
print('Counting... ', n)
print('Finished counting')
main()
Вывод этого кода:
Counting... 10
Counting... 9
Counting... 8
Counting... 7
Counting... 6
In the finally block
Finished counting
Гарантируется ли, что строка "в последнем блоке" будет напечатана до "завершения подсчета"? Или это из-за деталей реализации cPython, что объект будет собирать мусор, когда счетчик ссылок достигнет 0.
Также мне любопытно, как выполняется блок finally генератора countdown? например, если я изменю код main на
def main():
c = countdown(10)
for n in c:
if n == 5:
break
print('Counting... ', n)
print('Finished counting')
Тогда я вижу Finished counting напечатанным перед In the finally block. Как сборщик мусора напрямую переходит в блок finally? Я думаю, что всегда принимал try/except/finally за чистую монету, но размышления в контексте генераторов заставляют меня дважды подумать об этом.
2 ответов:
Вы, как и ожидали, полагаетесь на специфичное для реализации поведение подсчета ссылок CPython.1
На самом деле, если вы запустите этот код, скажем, в PyPy, вывод обычно будет:
И если вы запустите его в интерактивном сеансе PyPy, эта последняя строка может появиться много строк позже или даже только тогда, когда Вы наконец выйдете.Counting... 10 Counting... 9 Counting... 8 Counting... 7 Counting... 6 Finished counting In the finally block
Если вы посмотрите, как реализуются генераторы, у них есть методы примерно такие:
def __del__(self): self.close() def close(self): try: self.raise(GeneratorExit) except GeneratorExit: passCPython удаляет объекты сразу же, когда счетчик ссылок становится нулевым (он также имеет сборщик мусора для разбиения циклических ссылок, но это не имеет значения здесь). Как только генератор выходит из области действия, он удаляется, поэтому он закрывается, поэтому он поднимает
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 во втором тесте.
Я одобряю ответ @abarnert, но так как я уже напечатал это ...
Да, поведение в вашем первом примере является артефактом подсчета ссылок
CPython. Когда вы выходите из цикла, возвращаемый анонимный объект генератора-итератораcountdown(10)теряет свою последнюю ссылку, и поэтому сразу же собирается мусор. Это, в свою очередь, запускает наборfinally:генератора.Во втором примере генератор-итератор остается привязанным к
cдо тех пор, пока вашmain()не выйдет, так какCPythonзнает, что Вы можете возобновитьcв любое время. Это не "мусор", покаmain()не выйдет. Компилятор fancier мог бы заметить, чтоcникогда не упоминается после завершения цикла, и решить эффективноdel cдо этого, ноCPythonне делает попыток предсказать будущее. Все локальные имена остаются связанными до тех пор, пока вы явно не разорвете их сами, или область, в которой они являются локальными, не закончится.
Comments