генераторы Python сборка мусора - PullRequest
0 голосов
/ 30 апреля 2018

Я думаю, что мой вопрос связан с этим , но не совсем похож. Рассмотрим этот код:

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

Ответы [ 2 ]

0 голосов
/ 30 апреля 2018

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

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

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

0 голосов
/ 30 апреля 2018

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

...