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

Python 3.4 のオブジェクト開放処理

Python Advent Calendar 2013 の六日目です。Python3.4 で導入された PEP 442 -- Safe object finalization の解説を簡単に。

Pythonのメモリ解放処理

Pythonでは、処理中に使われなくなったオブジェクトを検出し、自動的に開放するようになっている。他のオブジェクトから全く参照されておらず、Pythonインタープリタからアクセスできなくなってしまっているオブジェクトは、不要なオブジェクトとして削除される。

例えば、

>>> class Spam:
...     pass
...
>>>
>>> obj = Spam()
>>> obj = None

では、最初に作成した Spam オブジェクトは、obj という名前で参照できるが、次の行では obj = None と別のオブジェクトを指すように変更されると、もうPythonインタープリタから参照できなくなる。このため、Pythonはこの Spam オブジェクトは不要となったと判断し、削除する。

循環参照

他のオブジェクトからの参照があっても、孤立したグループ内でのみ参照されているなら、そのグループのオブジェクトは開放される。

>>> class Spam:
...     pass
...
>>>
>>> obj1 = Spam()
>>> obj2 = Spam()
>>> obj3 = Spam()
>>> obj1.obj2 = obj2
>>> obj2.obj3 = obj3
>>> obj3.obj1 = obj1
>>> obj1 = obj2 = obj3 = None

この例では、obj1obj2obj3 オブジェクトは、それぞれお互いへの参照をメンバとして持っている。このように、複数のオブジェクトがループ状にお互いを参照することを、*循環参照* と呼ぶ。

この循環参照は、最後の obj1 = obj2 = obj3 = None で変数 obj1, obj2, obj3 が別のオブジェクトを参照するように変更されたので、どのオブジェクトもPythonインタープリタからはアクセスできなくなってしまった。Pythonは、このようにお互い同士では参照していても、外部からはアクセスできなくなってしまったオブジェクトも不要と判断し、開放する。

しかし、これまでのPythonでは、この循環参照の開放には制限があった。

>>> class Spam:
...     def __del__(self):
...         print('deleting...')
...         global dont_kill_me
...         dont_kill_me = self

このように、特殊メソッド __del__() を持つオブジェクトが循環参照を構成する場合、開放の対象外となっていた。これは開放のときに __del__() メソッドを呼び出すと、その呼出の順番によってはすでに解放済みのオブジェクトを参照してしまったり、また自分自身を再び外部から参照可能な状態にしたりしてしまう可能性があったためだ。

上の例では、__del__()グローバル変数 dont_kill_me にオブジェクトを保存しているので、Spam クラスのインスタンスは削除されることはない。

PEP 442

Python 3.4以降では、PEP 442 でこの点が改善され、__del__() を持つオブジェクトが循環参照に含まれていても、開放されるようになった。従来のメモリ解放処理に比べて、いくぶんか処理量が増加しているが、現在ではPythonガベージコレクションが実装された西暦2000年頃に比べてコンピュータのハードウェア環境は大きく進歩しており、この程度はあまり問題とはならないだろう。

PEP 442での変更で、以下の点は気にしておいたほうが良いだろう。


循環参照の一部に、先ほどの

>>> class Spam:
...     def __del__(self):
...         print('deleting...')
...         global dont_kill_me
...         dont_kill_me = self

のようなオブジェクトが含まれている場合、オブジェクトは参照可能な状態に戻るので開放はされない。しかし、この場合開放はされなくても、循環参照を構成する、全てのオブジェクトの __del__() メソッドが呼び出される。つまり、Python3.4以降では、__del__() が呼び出されたとしても、必ずオブジェクトが開放されるとは限らなくなった。


上の例で、すでに __del__() が呼び出されてしまったオブジェクトが再び参照不能になったときでも、もう __del__() メソッドは呼び出されない。__del__() が呼び出されるのは、ただ一度だけである。

(注: 例外として、 https://mail.python.org/pipermail/python-dev/2013-June/126834.html などのケースで複数回呼び出されることもありうるが、現実問題としてあまり考慮する必要はないだろう)

開放されない循環参照もある

PEP 442 で、すべての循環参照が開放されるようになったわけではない。循環参照に含まれるオブジェクトがPythonのクラスから作成したインスタンスだけであれば開放されるが、C言語で実装された型のオブジェクトが含まれる場合、従来と同じように開放されないケースもある。

油断は禁物

Python2で gc モジュールが実装されて以来、__del__() メソッドは実装するメリットよりもデメリットのほうが大きいと考えられ、できるだけ使わないようにするのが一般的だった。お陰で with ステートメントなどの機能が大きく進化することになったが、ここで再び __del__() のデメリットが少なくなったことで、__del__() にいろいろな処理を詰め込みたいと思う人もいるかもしれない。

しかし、__del__() のデメリットは、循環参照時に開放されないことだけではない。__del__() が呼び出されるタイミングは予測できないし、全く呼び出されないかもしれない。先程の例のように、呼び出されたとしても本当に開放されたとは限らないし、マルチスレッドアプリケーションでは、どのスレッドで呼び出されるかも予測できない。__del__() の活用には、いろいろと難しい点が残っている。

したがって、Pythonプログラミングのガイドラインとしては、「できるだけ __del__() に依存しない」というのは、依然として有効だと考える。くれぐれもC++のデストラクタと同じような使い方が出来ると思ってはならないのである。

Ubuntu 13.10 にVMWare Tools をインストールした時のメモ

Ubuntu 13.10 で普通にVMWare Fusion 6 の VMWare Tools をインストールしようとすると、

/vmhgfs-only/inode.c:1893:29: error: ‘struct dentry’ has no member named ‘d_count’
          int dcount = dentry->d_count;

こんな感じでエラーになってしまう。

こちらの記事 を参考に、VMWare Tools側のソースにパッチをあてるとうまく動くようだ

$ cd vmware-tools-distrib/lib/modules/source
$ tar xf vmhgfs.tar
$ curl -O https://raw.github.com/rasa/vmware-tools-patches/master/patches/vmhgfs/vmhgfs-d_count-kernel-3.11-tools-9.6.0.patch
$ patch -p0 < vmhgfs-d_count-kernel-3.11-tools-9.6.0.patch
$ mv vmhgfs.tar vmhgfs.orig.tar
$ tar cf vmhgfs.tar vmhgfs-only
$ cd ../../..
$ sudo ./vmware-install.pl

めんどくさいmock.patch()

unittest.mock モジュールを正しく使って関数を置き換えるというのは以外と難しいもので、Python名前空間について、しっかり把握できてないとうまくいかないことがある。

単純なケースでは、テスト対象のコードが参照している名前で置き換えてやればいい。 例えば

import spam

def ham():
    spam.egg()

というモジュール Mham() をテストするために spam.egg を置き換えるなら

def test():
    import M
    with patch("spam.egg"):
        M.ham()

となる。また、

from spam import egg

def ham():
    egg()

のように egg を参照している場合、ham() の内部での eggM.egg への参照なので

def test():
    import M
    with patch("M.egg"):
        M. ham()

となる。

ここで注意しなければならないのが、関数を置き換える対象となるモジュールは、関数を呼び出すときに指定するモジュールではなく、関数が定義されたモジュールだということだ。

例えばもう一つのモジュール、M2 があって、

import M
ham2 = M.ham

となっている場合がある。こんな場合でも、M2.ham2() で呼び出される egg() を置き換えるには

def test():
    import M2
    with patch("M.egg"):
        M2.ham2()

のように、ham を定義したモジュール Megg を置き換えなければならない。

このように、機械的に mock.patch() を使って置き換えることはできず、置き換える対象の関数がどのように呼び出されているか、ちゃんと調べてなくてはならない。普通はあまり気にしなくても良いのだが、たまに変な import などでどのモジュールを使っているのか調べにくいパッケージなどもある。そういう時は、

@contextlib.contextmanager
def patch_globalref(func, target):
   m = MagicMock()
      with patch.dict(sys.modules[func.__module__].__dict__, **{target:m}):
             yield m

のような関数を用意しておいて、

def test():
    import M2
    with patch_globalref(M2.ham2, 'egg'):
       M2.ham2()

とように、テスト対象の関数オブジェクトのグローバルスコープを直接修正してしまうほうが手っ取り早い場合もある。

ジェネレータの循環参照

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

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

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() を使ってフレームをどっかに保存するようなヤカラは、ひどい目にあえばいい気味というものである。

日本語でreStructuredText

実のところ、私はあんまり reStructuredText /Shinx が好きではない。Markdown と比べるとシンプルさで劣り、TeXと比べると印字品質で劣る。この辺の中途半端さが、restを愛せない大きな理由だが、もう一点、日本語の文章を書くのに適切なマークアップ言語ではない、というのも大きい。英語などの、単語と単語の間にスペースがある言語向けに設定されており、日本語で書こうとするとイライラが溜まってどうしょうもないのだ。

とはいえ、SphinxPythonの公式ドキュメントで使われているツールでもあり、ちょっと複雑なドキュメントを書くには Pandoc を使うよりは便利なことも多いので、ある程度は使えるようにしてみたい。ということで、私が使っていて一番イライラする、インラインマークアップの改善に取り組んでみた。

通常のrestでは、インラインマークアップの前後に空白や区切り文字が必要なため、

restは*うざい*!

とは書けない。

restは\ *うざい*\ !

などと書かなければならない。実にうざいので、スペース無しでもマークアップとして認識できるようにしてみた。

https://sourceforge.net/p/docutils/patches/103/

このパッチでは、前後のスペースや区切り文字などのチェックをいっさいチェックせず、`*` などの記号は全てマークアップとなる --no-inline-delimiters オプションを追加している。

$ rst2html.py --no-inline-delimiters sample.rst 

ディレクティブでも指定できる。

.. no-inline-delimiters yes

ここでは*普通に*マークアップを書ける

.. no-inline-delimiters no

ここでは\ *書けない*\ !

この方式で日本語環境でも使いやすくなったのではないかと思うが、どうだろうか?現在わかっている問題点として、アンダーラインを含んだ英単語がマークアップとして認識されてしまう、という点がある。

mod_wsgi

とすると、これは mod へのリンクと解釈されてしまうので

mod\_wsgi

と書かなければならない。許容範囲といえば許容範囲な気もするが、できれば治したい気もする。他にもなにか落とし穴があるかもしれない。気がついた点があれば、こちら で議論に参加していただきたい。

知らなかったが、Docutilsの開発者 David Goodger さんの奥様は日本人だそうで、Davidさんも日本語の知識がおありだそうだ。このパッチについても前向きに検討して頂いてる雰囲気なので、興味や意見があれば、ぜひともご協力いただきたい。