web-dev-qa-db-ja.com

pythonジェネレータガベージコレクション

私の質問は this に関連していると思いますが、まったく同じではありません。このコードを考えてみましょう:

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 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度考えさせられます。

27
skgbanga

予想どおり、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番目のテスト。

24
abarnert

@abarnertの答えを支持しますが、すでにこれを入力しているので...

はい、最初の例の動作はCPythonの参照カウントのアーティファクトです。ループから抜けると、返された匿名のジェネレーター/イテレーターオブジェクトcountdown(10)は最後の参照を失うため、すぐにガベージコレクションされます。次に、ジェネレータのfinally:スイートがトリガーされます。

2番目の例では、main()が終了するまで、ジェネレーターイテレーターはcにバインドされたままです。これにより、CPythonがあなたを知っている限り、 may cをいつでも再開できます。 main()が終了するまでは「ゴミ」ではありません。より洗練されたコンパイラ could は、cがループの終了後に参照されることはなく、その前に効果的にdel cを決定することに注意してください。ただし、CPythonは将来を予測しようとしません。ローカル名は、明示的にバインドを解除するか、ローカルであるスコープが終了するまで、バインドされたままになります。

16
Tim Peters