emptypage.jp > Notes

__del__, gc, 循環参照, weakref

2010年11月22日公開

Python の __del__ メソッド、ガベージコレクタ、循環参照、そして弱参照についての解説と考察。

参照カウンタ

C/C++ では、malloc を使って確保したメモリや new で作成したオブジェクトについては、必ず対応する free / delete を呼ばなければならない。これを忘れるといつまでも解放されないメモリがプログラムの生存期間中居残ることになる。これをメモリリークと呼ぶ。メモリリークがあると、確保されたままになったメモリが OS や他のプロセスを圧迫して、システム全体のパフォーマンスに影響を与えることがある。

Python (およびその他多くの軽量言語)では、基本的に作成したオブジェクトの解放について考えなくてもよい。使われなくなったオブジェクトに割り当てられていたメモリは、インタプリタによって自動的に解放される。

では、あるオブジェクトが「使われなくなった」ことを、インタプリタはどうやって判定しているのか。Python では、それは参照カウンタというもので実現されている。これは簡単に言うと次のようなものである:

オブジェクトの参照カウントは、sys モジュールの getrefcount 関数で取得できる。

C:\Users\mshibata>python
Python 2.7 (r27:82525, Jul  4 2010, 09:01:59) [MSC v.1500 32 bit (Intel)] on win
32
Type "help", "copyright", "credits" or "license" for more information.
>>> a = object()
>>> import sys
>>> sys.getrefcount(a)
2
>>> b = a
>>> sys.getrefcount(a)
3
>>> a = None
>>> sys.getrefcount(b)
2

最初の sys.getrefcount で帰ってくるのは 1 ではないのか、と思うかもしれない。しかしこれでいいのだ。なぜならオブジェクトが getrefcount の引数として渡されるときにその参照カウントがひとつ増えるからである。

__del__

オブジェクトの参照カウントが 0 になったとき、そのオブジェクトの __del__ メソッドが(もし定義されていれば)呼ばれる。その意味で、__del__ メソッドはデストラクタとも呼ばれる。このメソッドで、ファイルハンドルをクローズするとか、メンバ変数として保持していたリソースを解放するといった処理を行うことが想定されている。

とはいえ当然のことながら、メンバ変数として保持していたオブジェクトが Python のクラスに由来するインスタンスであるのなら(つまりたいていの場合は)、わざわざ __del__ メソッドを付けてそれを解放するような処理は必要ない(そんなことはやろうとしたこともないはず)。親にあたるオブジェクトへの参照が 0 になった時点で、そのメンバ変数のオブジェクトの参照カウントも適切に 1 ずつ減らされるからである。その結果が 0 になれば、メンバ変数として割り当てられていたオブジェクトも解放されることになる。

ドキュメントにも書かれていることだが、del x を実行したからといって、そのタイミングで x.__del__() が実行されるわけではないことに注意。del 文は、オブジェクトを指している変数名を名前空間から抹消して、そのオブジェクトへの参照カウントを減らすだけである。その結果参照カウントが 0 になれば、そのときはじめて __del__ が呼ばれる。

すでに書いたとおり、インスタンスの後処理として __del__ が必要になることはあまりない。あるとすれば、次のようなケースが考えられる。

__del__ の落とし穴

__del__ はその性質上、__init__ に対応するものと思えるかもしれないが、厳密にはそうではない。対応するものを挙げるとすれば、__init__ ではなく __new__ である(“Python Gotchas 1: __del__ is not the opposite of __init__”)。__init__ は、それが呼ばれた時点で、オブジェクトそのものが作成されたことは確定している。__init__ ではその初期化処理をするだけである。だから、__init__ が失敗したときでも、そのオブジェクトが解放されるタイミングで __del__ が呼ばれる。「__init__ が失敗したとき」というのは、つまりは __init__ 内で例外が発生したときである。したがって、注意深くなるのであれば、__del__ 内では __init__ の処理が行われていること(あるメンバ変数が存在することなど)を前提としてはいけないことになる。

例外といえば、また、__del__ で発生した例外は外部からは捕らえられないことにも注意。これは、そもそも __del__ が実行されるタイミングが決められないからである。__del__ 内で発生した例外が捕捉されなかったばあい、それはそのまま無視されてプログラムの実行が続く(例外が起こったこと自体は標準エラー出力で報告される)。

class C(object):
    def __del__(self):
        raise Exception('exception in __del__.')
        print 'can you see me?' # 例外が発生するので、この文は実行されない。

def main():
    a = C()
    del a
    print 'after "del a"' # この文は何事もなかったかのように実行され、プログラムはつつがなく終了する。

if __name__ == '__main__':
    main()

これにより、__del__ 内で解放したつもりのリソースがじつは解放されていなかった、ということが起こりうる。

だんだん __del__ を使いたくなくなってくる。

さらに、__del__ はインタプリタが終了する時点で残っているオブジェクトに対して呼ばれる保証がない。これはドキュメントにそう書かれている。そうなると、「絶対にぜったいにやらなきゃいけない終了処理」は、__del__ に書くことができないことになる。

循環参照

循環参照とは、オブジェクトが別のオブジェクトへの参照を保持しているという参照の連鎖が、どこかで一巡して元のオブジェクトに到達している状態のことである。いちばん単純な例は、ふたつのオブジェクトがお互いを参照しあっている、次のようなもの。

class C(object):
    pass

a = C()
b = C()
a.x = b
b.x = a   # 循環参照

ab への参照を持ち、ba への参照を持っている。

先に、オブジェクトの参照カウントが 0 になったときにオブジェクトは解放されると書いた。ところが循環参照があると、参照カウントの管理はとたんに難しい問題を抱える。なぜなら、上の例でいえば、b の参照カウントは変数 a.x がなくならない限り(つまり a が解放されない限り) 0 にならないが、その a といえば b.x の参照カウントが 0 にならない限り解放されないからである。

さて、実際には、Python は頭のいい工夫によって、参照カウントを減らすことはできないものの「どこからも参照されなくなった」オブジェクトを検出して、それを解放している(“Garbage Collection for Python” / 日本語訳「PythonのGCについて」)。このため、循環参照によるデッドロックはふつうは発生しない。オブジェクトがいくつも組み合わさった複雑なクラスを使ったり、メソッドを変数として渡す処理などをしていると(メソッドは元のインスタンスへの参照を保持している)、プログラマが気づいてないうちに循環参照が発生していることもあるが、やはりまた気づかない間にそれらは問題なく解放されており、メモリリークは発生しない。

次のような(あまり意味のない)コードを実行してみると、プログラムは無限に走り続けるが、タスクマネージャで確認してみればわかるとおり、使用メモリ量はある値以上にはならない。ガベージコレクタが、使われなくなった循環参照しているオブジェクトをちゃんと解放しているからである。

class C(object):
    pass

def main():
    while 1:
        a = C()
        b = C()
        a.x = b
        b.x = a

if __name__ == '__main__':
    main()

__del__ の落とし穴

しかし、この循環参照を解決するガベージコレクションの仕組みを適用できない場合が存在する。それは、オブジェクトに __del__ メソッドが定義されている場合である。__del__ メソッドを持つふたつのオブジェクト a, b が互いへの参照を保持しあっている(循環参照している)場合を考えてみよう。ガベージコレクタは、どちらのオブジェクトの __del__ を最初に呼ぶべきだろうか。a.__del__() の中で、b の値を利用しているかもしれない。すると、b を最初に解放するべきか。しかし同様に b.__del__() の中で、a の値を利用しているかもしれない。各オブジェクトの __del__ メソッドを呼ぶ順番は、機械的には決められないのだ。このため、Python のガベージコレクタは、__del__ メソッドを持つオブジェクトが循環参照している場合には、そのオブジェクトを自動では解放しない。これらのオブジェクトは、プログラム上で循環参照関係を明示的に解決しない限り、(上の例でいえば、a.x = None; b.x = None などとしない限り、)メモリ上に存在し続ける。

先ほど自動的に解放される循環参照を確認する例として挙げたコードを少しだけ書き換えてみる。

class C(object):
    def __del__(self):
        pass

def main():
    while 1:
        a = C()
        b = C()
        a.x = b
        b.x = a

if __name__ == '__main__':
    main()

クラス C__del__ メソッドを付けただけのものだが、これを実行すると、恐ろしいことが起こる。

タスクマネージャで見ると、Python インタプリタの使用メモリ量がどんどん増えていく。

みるみるうちにメモリを食い尽くし、そのうち MemoryError 例外が発生して、プログラムは異常終了する。__del__ メソッドを定義しているオブジェクトでは、循環参照を残して放置してはいけないのである。

考察

さて、(これは筆者が考えたわけではなく Imri Goldberg 氏のブログから拝借してきたものだが、)これまでのことから、「__del__ + 循環参照 = リーク」という公式が導かれる。問題となる事態を避けるために、どのような対策が考えられるだろうか。

__del__ を使わない

クラスで __del__ を定義しない。基本的にはこれがいちばん安全である。__del__ がないのであれば、プログラマは基本的にメモリリークの心配をする必要はない。ドキュメントに「後処理は自動で行われる」と書いてあるオブジェクト(たとえば file オブジェクト)に対して、なんとなく気持ち悪いからという理由だけで安易に __del__ メソッドを付けて明示的な後処理を書いたりするのはかえって危険な可能性がある。また、解放が必要なリソースをメンバ変数として確保する場合は、そのクラスに open / close などのメソッドを定義して、クラスを利用する側で明示的にそのメソッドを呼ばせるようにすることを考える。

循環参照を確実に避けることはできる?

__del__ を定義していなければ、循環参照があってもオブジェクトは「いずれ」解放される。しかし、その「いずれ」がいつかは、はっきりと知るすべはない。ブログ “Geek at Play” によれば、サーバ用のコードなどで負荷の高い処理が続くような場合、解放のタイミングがなかなか訪れずリークしたのと同様のメモリの逼迫した状況が発生することもあるという。(“Finding my first Python reference cycle”

意識せずに循環参照を作ってしまう例として多い(と思われる)のが、“Geek at Play” でも紹介されていた、コールバック用のハンドラ関数としてメソッドを他の変数に代入するというケースだ。メソッドは関数のように扱えるが、考えてみれば当然のことながら、それ自身にオブジェクトのインスタンスへの参照を保持している。このため、ハンドラを登録したオブジェクトが自身のメンバ変数だったりすると、循環参照ができてしまう。

urllib2 にメモリリークがあるという話があるそうだが、それもメソッドの代入による循環参照が原因のようだ。(注意:ここでいう「リーク」は厳密な意味ではリークではない、循環参照のオブジェクトはいずれ解放されるので。しかし、先に述べたようにガベージコレクタが働くタイミングは決定できない。このため、ループなどで循環参照しているオブジェクトを大量に作ってしまうとガベージコレクタが動く前にメモリを使いつぶしてしまうのである。)

もちろん循環参照を避けることができるのであれば、それは望ましい。しかし、循環参照を確実に回避するこれといった方法はあるのだろうか。簡単なチェック機構を書けば、相互参照レベルの循環参照は見つけられるかもしれない。しかし、実際のコードでは非常に長いステップの循環参照が発生しうるし、それに気づくのは場合によってはとても難しい。ソフトウェア会社 LShift のブログによると、 かれらがツールを使ったデバッグの末にようやく見つけた循環参照は 9 ステップあったという(記事の図を参照のこと)。こういうものを見ると、循環参照の発生を予測するのは不可能ではないかという気さえしてくる。

個人的には、起こりうる循環参照を避けることを意識しながらコードを書く、あるいはそれを検出することを試みるのは、無理のある話のような気がするのだ。

弱参照

Python には、オブジェクトへの弱参照を作る、weakref モジュールが用意されている。弱参照というのは、オブジェクトそのものを保持するのではなく、後からそのオブジェクトへの参照を取り出せるような一時的なチケットを持つような仕組みだ。これにより、オブジェクトの参照カウントを増やすことなく、必要に応じてそのオブジェクトへアクセスする手段を持つことができる(ただし、参照カウントを増やさないということは、必要なときにそのオブジェクトがすでになくなっている場合もあるということで、プログラムではそのことも考慮しなければならない)。

相互参照をしている場面でも、片方が相手への弱参照を持つようにすれば循環参照にはならない。ツリー構造で親子が相互に相手の参照を保持しあっているようなケースなど、場合によってはこのアプローチが有効な場面もあるかもしれない。

weakref モジュールを利用したハック

とはいえ、C ライブラリのラッパなどを作成していると、__del__ メソッドを使いたい場面は多い。そんなことを考えていたら、最近になって weakref モジュールを利用したハックで、__del__ を定義することなしにオブジェクトの解放時に任意の関数を実行させるレシピが公開されているのを見つけた(“Calling (C-level) finalizers without __del__”)。

これは、weakref.ref の(あまり使われない)第 2 引数のコールバックを利用したものだ。詳しくはリンク先のページを見ていただくとして、メンバ変数として確保したリソースを解放するためのコードを、親オブジェクトが解放されるタイミングで実行するというものである。

ただしこの処理も万能ではない。たとえば、この方法で解放処理を予約されたメンバ変数が、x = a.to_be_finalized のようにそれだけで取り出されて代入されたらどうなるだろう。予約された a.to_be_finalized の解放処理は a が解放されるタイミングで走るので、代入してとっておいた x を利用するときには、x はすでに無効なオブジェクトとなっている可能性がある。それでも、こうした代入を避けるなどの工夫をして気を付けていれば、有効に思う。(疑問:弱参照の callback はインタプリタ終了までに呼び出されることは保証されてるのかしら?)

いったいいつ __del__ を使うのか

考えれば考えるほど、__del__ の使いどころは難しい。呼ばれる保証がないところから、絶対にやらなければいけない終了処理を記述することはできない。すると、「どうせインタプリタが終了してしまえば同じことだが、いまやっておくとまあちょっといい処理」を書けばいいのかということになる。たとえばファイルをクローズするなどの処理(ファイルをオープンしたハンドルは、プログラムが終了するとすべて自動的にクローズされる)。

ところが、そういう「やっておくといいかもしれない」程度の処理を書くには、__del__ は代償(循環参照によるメモリリーク)が大きすぎるように思う。しかし、かといってファイルを閉じる程度の終了処理に weakref ファイナライゼーションのハックを導入するのも、なんだか仰々しい気がする。クラス設計が十分に自己完結していて、利用形態も単純なものに限られる(すなわち循環参照の発生が想定しにくい)のであれば、__del__ は容認できるかもしれない。ここで想定しているのは、C ライブラリや API のラッパ・モジュールなどである。対して、メンバ変数にたくさんのオブジェクトを保持し、複雑な参照関係を持つクラスを設計しているときに、__del__ を定義しようとするのはものすごく危険な兆候ということになる。

結局、「いつでも常にこれをやればオーケー」というやり方は存在しない、というどうしようもない結論になるのかもしれない。いずれにせよ必要なのは __del__ とガベージコレクタの仕様と挙動を理解しておくことで、そうなっておけば、将来予期しないハングアップやメモリリークを解決することができる(かもしれない)。

参考文献

事項

Python 開発コミュニティと、さまざまな解説記事やサンプルコードを公開されている方々に感謝します。

内容については正確さを心がけていますが、知識不足により間違ったことを書いてしまっているかもしれません。記事の内容によって生じた損害等について作者はその責任を負いません。ご了承ください。記事に関するご指摘やご意見ご感想はいつでも歓迎です。ゲストブックやメールなどでご連絡ください。

クリエイティブ・コモンズ・ライセンス この文書は、クリエイティブ・コモンズ・ライセンスの下でライセンスされています。また記事に掲載しているサンプルコードはすべてパブリックドメインに置きます。

Copyright 2010 Masaaki Shibata <mshibata at emptypage.jp>