私の質問は 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度考えさせられます。
予想どおり、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)
は最後の参照を失うため、すぐにガベージコレクションされます。次に、ジェネレータのfinally:
スイートがトリガーされます。
2番目の例では、main()
が終了するまで、ジェネレーターイテレーターはc
にバインドされたままです。これにより、CPython
があなたを知っている限り、 may がc
をいつでも再開できます。 main()
が終了するまでは「ゴミ」ではありません。より洗練されたコンパイラ could は、c
がループの終了後に参照されることはなく、その前に効果的にdel c
を決定することに注意してください。ただし、CPython
は将来を予測しようとしません。ローカル名は、明示的にバインドを解除するか、ローカルであるスコープが終了するまで、バインドされたままになります。