Python3で例外をキャッチすると、例外オブジェクトを代入した変数は削除される。
>>>a =100 >>> a 100 >>> try: ... 1/0 ... ... except Exception as a: ... pass ... >>> a Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'a' is not defined
Python3で例外をキャッチすると、例外オブジェクトを代入した変数は削除される。
>>>a =100 >>> a 100 >>> try: ... 1/0 ... ... except Exception as a: ... pass ... >>> a Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'a' is not defined
Python Advent Calendar 2013 の六日目です。Python3.4 で導入された PEP 442 -- Safe object finalization の解説を簡単に。
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
この例では、obj1
、 obj2
、obj3
オブジェクトは、それぞれお互いへの参照をメンバとして持っている。このように、複数のオブジェクトがループ状にお互いを参照することを、*循環参照*
と呼ぶ。
この循環参照は、最後の 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
クラスのインスタンスは削除されることはない。
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 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
unittest.mock
モジュールを正しく使って関数を置き換えるというのは以外と難しいもので、Pythonの名前空間について、しっかり把握できてないとうまくいかないことがある。
単純なケースでは、テスト対象のコードが参照している名前で置き換えてやればいい。 例えば
import spam def ham(): spam.egg()
というモジュール M
の ham()
をテストするために spam.egg
を置き換えるなら
def test(): import M with patch("spam.egg"): M.ham()
となる。また、
from spam import egg def ham(): egg()
のように egg
を参照している場合、ham()
の内部での egg
は M.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
を定義したモジュール M
の egg
を置き換えなければならない。
このように、機械的に 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()
とように、テスト対象の関数オブジェクトのグローバルスコープを直接修正してしまうほうが手っ取り早い場合もある。
Windows用のコマントプロンプトの置き換えアプリ Console の、IME対応パッチ版
https://github.com/atsuoishimoto/console2-ime
2013-07-06版(Console 2.00 b148用)
https://github.com/atsuoishimoto/console2-ime/releases/tag/console-2-b148-ime
以前、ジェネレータが循環参照の一部になっている場合、メモリが開放されなくなるケースがあるという エントリ を書いた。
最近、この仕様が 問題となっていた ようで対策が検討されていたが、ついにトランクにコミットされたようだ。
従来のジェネレータでは、ジェネレータオブジェクトへの参照が全て破棄されて不要となった時に
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()
を使ってフレームをどっかに保存するようなヤカラは、ひどい目にあえばいい気味というものである。