thisとメソッド

昔よく似た話を書いた↓けど、まあいいや。最近再利用が多いなー
http://d.hatena.ne.jp/w_o/20050824#p2


JavaScriptとかPHPとかのキモいといわれている点に、メソッドがオブジェクトとひとつになってないというのがある。

<script>
function Nanika()
{
	this.i = 40;
	this.method = function() { return this.i; }
}


var a = new Nanika();
document.write( a.method() + "<br>");

var obj = { i:100 };
obj.method = a.method;
document.write( obj.method() + "<br>" );
</script>

a.methodは、a.iを参照してる…と思うのだけど、二回目のdocument.writeでは、objのiの値が表示されてしまう。
なんでこうなるのかというと、このほうが効率よく実装するのが簡単だから。(実装によるけど。多分)


JavaScriptのように、メソッドがthisを保存しないような場合の実装は、

  • クラスごとにメソッドを持つ
  • オブジェクトが持ってるのは、クラスが持ってるメソッドへのポインタのみ

こんな感じのデータ構造にして、

a.b()

こういう場合の処理を、「aをthisにして、メソッドbを呼び出す」っていう感じで、ひとつにまとめてしまう、と。
仮想マシン命令にすると、

push_global [symbol a]
call_method_with_obj [symbol b]

こぉぉーーーんな感じでできあがり…って、どんな説明だよ。
とにかく、それなりに簡単にできる、というわけ。


ところが、「メソッドの呼び出しかたによって、thisが変わる」というJavaScriptみたいな変な挙動を防ぐには、メソッドごとに「そのメソッドのthisはなんなのか」というのを記憶しとかないといけない。

struct method_with_this {
	struct object *this;
	struct method_body *body;
};

こんなのがいります。と。
これが、「オブジェクトの数 x メソッドの数」だけ必要。4個のメソッドを持つオブジェクトが20個あったら、80個分のmethod_with_thisが必要。ポインタが4byteだとしたら、640byte。
さらに、method_with_thisは、1wordに収まらないので、この640byteは、それぞれ、一個づつヒープから取ってこないといけない。(ほんとかよ。シンボルテーブルが持てる値を2wordにすればいけるよ。なんかうまくやれば良いような気がしてきた)


さて、これは、どのぐらいの無駄だろうか。「オブジェクトから、メソッドだけを取ってくる」っていうのは、たまーに必要だけど、大体の場合で必要じゃないよね。多分。あと、JavaScriptだと、

function new_method( obj, method ) {
	return function() { return obj.method(); }
}

var m = new_method( a, a.method );
document.write( m() + "<br>" );

クロージャを使うという手段があるわけで、なんか、まあ、十分許容範囲だよね。と、だから、簡単に実装できるほうでいいや、ということで、上で書いたような言語仕様になってるのだと思う。


ところが、Python
Pythonは、JavaScriptとかと違ってメソッドがthisを保存してるのである。

class Nanika:
	def __init__(self):
		self.i=40
	def method(self):
		return self.i

	
class Nanika2:
	def __init__(self):
		self.i=100

		
a = Nanika()
b = Nanika2()
b.method = a.method
b.method()
40

えー。

def ref_method2(a):
	return a.method()

dis.dis(ref_method2)
          0 LOAD_FAST                0 (a)
          3 LOAD_ATTR                0 (method)
          6 CALL_FUNCTION            0
          9 RETURN_VALUE

バイトコード見ても、変なトリックもなく。
というわけで、Pythonのメソッドって大変メモリを使ってそうな気がしたのです。


これのトリックは、Pythonが、参照カウントを使ってるという点。
LOAD_ATTR(method)で、メソッドが参照される。
このとき、メソッドのデスクリプタget(仕組みは知らないけど…__get__が云々)が呼び出されて、新規にメソッドが割り当てられる。(メソッドが割り当てられるのは、オブジェクト生成時ではなく、メソッド参照時になる)
んで、CALL_FUNCTION。CALL_FUNCTIONが終わると同時に、参照カウントが0になって、メソッドは解放される。と、メソッドが生きてる期間が非常に短いので、あんまりメモリの無駄が無いという。
あと、メモリ割り当ても、メソッドだけ特別扱いされてて、

/* Objects/classobject.c */
static PyMethodObject *free_list;

PyObject *
PyMethod_New(PyObject *func, PyObject *self, PyObject *klass)
{
	register PyMethodObject *im;
	if (!PyCallable_Check(func)) {
		PyErr_BadInternalCall();
		return NULL;
	}
	im = free_list;
	if (im != NULL) {
		free_list = (PyMethodObject *)(im->im_self);
		PyObject_INIT(im, &PyMethod_Type);
	}

	... 

なんかこんな。


参照カウントだからできる無茶もあるということで。