このBlogは移転しました。今後は aish.dev を御覧ください。

ジェネレータの循環参照

以前、ジェネレータが循環参照の一部になっている場合、メモリが開放されなくなるケースがあるという エントリ を書いた。

最近、この仕様が 問題となっていた ようで対策が検討されていたが、ついにトランクにコミットされたようだ。

http://bugs.python.org/issue17807

従来のジェネレータでは、ジェネレータオブジェクトへの参照が全て破棄されて不要となった時に GeneratorExit 例外を送出してコードの実行を再開し、ジェネレータ内の finally 節が実行されるようになっていた。Pythonのコードで書くと、こんな感じになる

class Generator:
   def __del__(self):
      if self.generator:
         self.generator.send(GeneratorExit)

ここで、ジェネレータオブジェクトは __del__ メソッドを持っているため、このオブジェクトが循環参照の一部となっていた場合、ガベージコレクションでは開放できない、というのが上のエントリの話だった。

Python3.4では、上のコードの __del__() メソッドに相当する部分が削除され、代わりに ジェネレータ を実行中のフレームオブジェクトを開放するときに終了処理が起動されるようになった。このため、ジェネレータオブジェクトが循環参照に巻き込まれても、オブジェクトは保存されずに開放されるようになった。

終了処理をジェネレータからフレームに移すとなんで循環参照の問題が解決するのか不思議だったが、パッチを見ると、終了処理が必要なジェネレータでも、リファレンスカウントがゼロになったら無慈悲に削除してしまうようだ。フレームオブジェクトの開放処理を見ると、実行中のコードオブジェクトとそのフレームオブジェクトだけ使って、フレーム開放時にジェネレータの finally 節を実行できてしまうのだ。ジェネレータの後始末をするのにジェネレータ自体は不要だったというのは実に面白い。

ということで、このまま問題がなければ、Pytho 3.4からは安心してジェネレータをあっちこっちから参照できるようになったわけだ。もちろん、フレームオブジェクトを下手に使うとやはり循環参照になってしまうが、これはジェネレータだけの話ではないし、うかつに sys._getframe() を使ってフレームをどっかに保存するようなヤカラは、ひどい目にあえばいい気味というものである。