2010-07-03

関数とメソッドの違い

オブジェクト指向言語におけるメソッドを関数で実装できるだろうか?

最近のオブジェクト指向言語では,関数型言語の特徴であるファーストクラスオブジェクトとしての関数が提供されていることが多い.このような言語の場合、メソッドは関数なのだろうか?インスタンス変数に関数を代入すればそれが即ちメソッドなのだろうか?しかしことはそう単 純ではない.レシーバ (C++, Java, JavaScript における this) の扱いをどうするかという問題があるからである.

JavaScript

JavaScript ではまさにメソッドが関数として実装されている.

o = {
    data: "hello",
    method: function () { alert(this.data); }
};
o.method();

とすれば "hello" が表示される.それでは以下のように一旦関数を単離するとどうなるだろうか?関数がファーストクラスオブジェクトであるからには単離して持ち運ぶことが可能でなければならない.

m = o.method;
m()

この場合 undefined が表示される.いったい method 中の this は何を指しているのであろうか?引き続いて

data = "world";
m()

を実行してみれば分かる通り ("world" が表示される),この場合の this はグローバルオブジェクト (window) を指している.次に全然別のオブジェクトに持って行ってみよう.

oo = {
    data: "!",
    method: o.method
};
oo.method();

今度は "!" が表示される.つまり JavaScript では呼び出し方に応じて this が指すものを変えることでメソッドを関数として実装することを可能にしていると言える.

JavaScript の仕様書では,object.variable は単純な値へと評価されるのではなく,(object の値, "variable") という組へと評価されると説明されている (8.7 The Reference Type). この組に対して関数呼び出しが適用される (object.variable()) と,this が「object の値」を指すように設定されて,object["variable"] というプロパティ値の関数が呼び出されることになる (11.2.3 Function Call). この仕様を見れば上記の挙動も理解できるだろう(「object の値」が無いときには this はグローバルオブジェクトを指す,という記述が仕様書にある).

ちなみにこの Reference Type はカッコ式を素通りするので,

(o.method)()

のときも thiso を指し,"hello" が表示される.ただしコンマ演算子が適用されるとプロパティ値へと変換されるので,

(1, o.method)()

のときの this はグローバルオブジェクトを指し,"world" が表示される.かなり高度な JavaScript パズル問題といえるだろう.

Python

Python では,メソッドのレシーバをプログラマが明示的に宣言しなければならない点で JavaScript よりもさらに単純にメソッド=関数となっているように見える.

class Foo(object):
    def __init__(self):
        self.data = "hello"
    def method(self):
        print self.data
foo = Foo()
foo.method()

とすれば当然 "hello" が表示されるが,以下はどうだろうか?

m = foo.method
m()

class Bar(object):
    def __init__(self):
        self.data = "world"
    method = m
bar = Bar()
bar.method()

いずれも "hello" が表示される.Python では JavaScript のように () による関数呼び出しの時点でレシーバが決まるのではなく,インスタンス変数の内容を取得する時点でレシーバが決定されるようになっている.

以下ではわかりやすいように,メソッド関数をグローバルで定義して実験してみる.

def m(self):
    print self.data

class Foo(object):
    def __init__(self):
        self.data = "hello"
    method = m

class Bar(object):
    def __init__(self):
        self.data = "world"
    method = m

foo = Foo()
bar = Bar()
print foo.method == m
print bar.method == m
print foo.method == bar.method

この場合いずれも False が表示される.このように,同一の関数をインスタンス変数に設定したにもかかわらず,インスタンス変数の内容を取得して比較すると異なっていることが分かる.

実は Python ではインスタンス変数へのアクセスの際に,それがユーザ定義関数であれば,ユーザ定義メソッドという別のオブジェクトが返される (Python リファレンス「3.2 標準型の階層」の「ユーザ定義メソッド」).このオブジェクト (o とする) には o.im_func に元の関数が,o.im_self にレシーバがセットされ,メソッド呼び出し時の関数とレシーバとして用いられる.

Python のメソッド=関数かつレシーバ引数の明示という仕様はシンプルできれいかもしれないが,そのしわよせは別の場所,しかも仕様書のどこにあるかもよくわからないような所に隠されている.

まとめ

関数でメソッドを実現することは一見単純なことのように思えるが,実際はそうでもないということがわかる.

Perl や Ruby でどうなっているかを調べてみるのもおもしろいだろう.

0 件のコメント:

コメントを投稿