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 за чистую монету, но размышления в контексте генераторов заставляют меня дважды подумать об этом.

555   2  

2 ответов:

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

Я одобряю ответ @abarnert, но так как я уже напечатал это ...

Да, поведение в вашем первом примере является артефактом подсчета ссылок CPython. Когда вы выходите из цикла, возвращаемый анонимный объект генератора-итератора countdown(10) теряет свою последнюю ссылку, и поэтому сразу же собирается мусор. Это, в свою очередь, запускает набор finally: генератора.

Во втором примере генератор-итератор остается привязанным к c до тех пор, пока ваш main() не выйдет, так как CPython знает, что Вы можете возобновить c в любое время. Это не "мусор", пока main() не выйдет. Компилятор fancier мог бы заметить, что c никогда не упоминается после завершения цикла, и решить эффективно del c до этого, но CPython не делает попыток предсказать будущее. Все локальные имена остаются связанными до тех пор, пока вы явно не разорвете их сами, или область, в которой они являются локальными, не закончится.

Comments

    Ничего не найдено.