сборщик мусора 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  

Гарантировано ли, что строка "В блоке finally" будет напечатана перед "Готовым подсчетом"? Или это из-за детализации реализации 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 по его номинальной стоимости, но мышление в контексте генераторов заставляет меня дважды подумать об этом.

Ответ 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-компилятора во втором тесте.

Ответ 2

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

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

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