読者です 読者をやめる 読者になる 読者になる

Pythonのselfはなぜ必要かをJavaScriptのthisで考える

Python JavaScript

あなたがもしPythonを作る前のGuidoに憑依して - ネットリサーチ - livedoor ニュース が面白すぎた。2位と3位の

  • すべてを式にする
  • lambdaの構文を変える

は、同じ願いを別の言い方でしてるような気がした。lambdaにifとforを入れたいをかなえるには、ifとforを式にするか、lambdaに文が入るようにするか、どちらか一方だし。


それはさておき、このエントリの本題は、「Pythonにはselfが要る」というGuidoさんの主張について、具体例で理解することです。「こうだったらいいのにな」逆の視点、もしselfがないとどう困るのか、を考えましょう。

そこで、Pythonとは別の母親から産まれた双子、JavaScriptを例に、thisについて考えてみます。Pythonに対して、JavaScriptは「メソッド定義の第一引数に余分なアレがないこと」が特徴でしたね。JavaScriptだと、いつの間にか勝手にthisが発生している。

例題:文字列をHTMLタグで囲むオブジェクト、Tagを実装してみる。

function Tag(type){
    this.type = type;
}
Tag.prototype.wrap = function(content) {
    var tname = this.tagName();
    return "<" + tname + ">" + content + "</" + tname + ">";
};
Tag.prototype.tagName = function() {
    return this.type.toLowerCase();
};
var ptag = new Tag("P");
ptag.wrap("Hello");  // ...これは "<p>Hello</p>" になる。

tagNameはwrapしか使わないメソッドなので(いや実際はそうじゃないんだけど、仮にそうだと仮定して)、wrapの内部に移動させます。

function Tag(type){
    this.type = type;
}
Tag.prototype.wrap = function(content) {
    function tagName() {  // メソッド隠しちゃえ
        return this.type.toLowerCase();
    }
    var tname = tagName();
    return "<" + tname + ">" + content + "</" + tname + ">";
};
var ptag = new Tag("P");
ptag.wrap("Hello");  // ギャー undefined でたー

this.type.toLowerCase() でundefined発生。ありがちなケアレスミスなので、修正しよう。

function Tag(type){
    this.type = type;
}
Tag.prototype.wrap = function(content) {
    function tagName() {
        return this.type.toLowerCase();
    }
    var tname = tagName.call(this);  // 呼び出しコンテキストとしてthisを与える
    return "<" + tname + ">" + content + "</" + tname + ">";
};
var ptag = new Tag("P");
ptag.wrap("Hello");  // どうにかなった

なくしたthisを取り戻すのに、まあまだ、このぐらいなら許そう。

これが高階関数に行くともっと面倒。

//Tagのメンバじゃない関数があるとしよう
function format(func, content){
   var tagname = func.call(..., content);
   //う、Tagのインスタンスがない
   //ごめん、なくしちゃったみたい
   return ...; // ry)
}

//Tagクラスの別のメソッドの中で
format(this.wrap, "Hello");  //やれやれ、またthis紛失だよ

関数はインスタンスにひもづいていないので、これそのまま解決するには、行き先にthisも渡さなきゃいけない。

//Tagのメンバじゃない関数があるとしよう
/*
 * @param func_this_scope funcの呼び出しでthisにあたるコンテキスト
 */
function format(func_this_scope, func, content){
   var tagname = func.call(func_this_scope, content);
   return ...; // (ry
}

//Tagクラスの別のメソッドの中で
format(this, this.wrap, "Hello");  //なんとか動いた、ひぃひぃ

引数2個とは、えらく面倒なAPI仕様になったもんだ。

ところで、formatにオブジェクトとメソッドじゃなくて普通の関数を渡したいとき、第一引数はnullにするのかな? なんか、クライアントコードが汚くなりそうだなぁ。

※ いや、実は、冗談ぬきで、ActionScript2には本当にこういうAPIがある。

ありえないけど、こんなのも…

function format(obj, method_name, content){
   var tagname = obj[method_name].call(obj, content);
   return ...; // (ry
}
format(this, 'wrap', "Hello");

これじゃ、もはや「後悔」関数だよ。素の関数を渡そうにも、本当にどうしようもない。


などをふまえたうえで、関数型手続き言語では、クロージャを使い、より自然にレキシカルスコープでやるのが定石。JavaScriptで頻出パターン。

function Tag(type){
    this.type = type;
}
Tag.prototype.wrap = function(content) {
    var self = this;  // クロージャに補足できる場所に変数を起く
    function tagName() {
        return self.type.toLowerCase(); // コンテキストはクロージャから取得するので、
    }
    var tname = tagName();  // なにも気にせず、状態依存関数じゃないつもりで書いておk
    return "<" + tname + ">" + content + "</" + tname + ">";
}
var ptag = new Tag("P");
ptag.wrap("Hello");   // 期待通りになる
//Tagのメンバじゃない関数があるとしよう
function formatter(func, content){
   var tagname = func(content);
   return ...; // (ry
}

//Tagクラスの別のメソッドの中で
var self = this;
formatter(function(content){ return self.wrap(content);}, "Hello");

ちなみに、ActionScript2以前のthisはJavaScriptと同じ、ただの呼び出しコンテキストで、ActionScript3のthisはここでいうレキシカルスコープのselfと同じものを指す。Flashはややこしいなあ。


ここで、Pythonの登場。

JavaScriptの最初の解法と同じもの。

class Tag(object):
    def __init__(self, type):
        self.type = type
    def wrap(self, content):
        def tagName(self):      #特定のオブジェクトを扱うつもりの関数
            return self.type
        tname = tagName(self);  #...に、自分を渡す
        return "<%s>%s</%s>" % (tname, content, tname)

ptag = Tag("P")
ptag.wrap("Hello")

何をどう渡すか明確だ。

二番目の解法と同じもの。

class Tag(object):
    def __init__(self, type):
        self.type = type
    def wrap(self, content):
        def tagName():      #クロージャが握る環境にある変数を使う関数
            return self.type
        tname = tagName();  #...には、何も渡さなくてもいい
        return "<%s>%s</%s>" % (tname, content, tname)

ptag = Tag("P")
ptag.wrap("Hello")

レキシカルスコープにselfが最初から存在する。

さらに、高階関数でもこんなにシンプル。まず、インスタンスと関数を別々に渡す例。

#Tagのメンバじゃない関数があるとしよう
def format(obj_as_self, func, content){
   tagname = func(obj_as_self, content)
   return ... # (ry
}

#Tagクラスの別のメソッドの中で
format(self, Tag.wrap, "Hello")

暗黙なthisがなく、つねにselfが明示的に存在するので、

tagname = func(obj_as_self, content)

がそのまま、

Tag.wrap(self, "Hello")

を実行するんだということが、一目でわかる。引数の位置もずれていない。

次は、インスタンスバインド済みの関数を渡す例。

#Tagのメンバじゃない関数があるとしよう
def format(func, content){
   tagname = func(content)
   return ...; // (ry
}

#Tagクラスの別のメソッドの中で
format(self.wrap, "Hello")  # 短っ!

ここで、JavaScriptとの決定的な違いに注意。self.wrapのように、フィールドとして、「インスタンス」からメソッドを取り出すと、インスタンスがバインド済みの(すでにカリー化された)関数が出てくる。JavaScriptで同じように書いても、生の関数(this.constructor.prototypeに保存された関数そのまま)しか出てこない。

a = self.wrap                           # selfのwrapメソッド
b = lambda *args: Tag.wrap(self, *args) # Tagクラスのwrapにselfを部分適用したもの
#厳密には実装が異なるが、aもbも意味は同じ
a('hoge') == b('hoge')                           # >> True
a('hoge') == b('hoge') == Tag.wrap(self, 'hoge') # >> True

この置き換えを使って、formatter(self.wrap, "Hello") の部分をわざわざJavaScriptと同じように書くと、こういうことになる。

self_as_this = self
wrap_of_self = self.__class__.wrap
formatter(lambda *args: wrap_of_self(self_as_this, *args), "Hello")

PythonOOPするとき、「関数」だけで明確に説明できる仕様だし、素直に書いたものが最も問題が少なくなる。そのうえ、関数の移動や変数の改名など、機械的なパターン作業でリファクタリングできる可能性が非常に高い。引数の位置がずれたり、関数の呼び出しを、thisコンテキストを与えるcallに置き換えたりしなくても済むからだ。初心者が見えないthisを理解していないことから来る問題もないだろう。

JavaScriptのthisで迷子になるぐらいなら、
「いまオレは、第一引数に主体オブジェクトを取る素の関数を書いているだけ」
と自分に言い聞かせながらselfと綴るほうがマシです。べつにこれ、間違った表現でもなんでもなく、まさにそのとおりなんだし。なんなら、クラスの外でそんな関数作って、動いてからクラスの中に「そのまんま」移動(本当に何も書き換える必要がない)させてもいいですし。

Rubyがselfを持たないのは、インスタンスとメソッドが常に強力に結びついているからで、C++Javaと同じですね。インスタンスにファーストクラスな関数を糊付けするような言語で、Javaと同じことをしようとしても不可能です。「関数をリテラルで作成でき、変数に入れて取り回せる」「ネストする関数定義が真のクロージャである」という特性を活かしたいと思うかぎり、OOP特化言語だからこそ可能な構文最小化までは、どうしてもできないということです。でないと、JavaScriptのように、thisの迷子に苦しむことでしょう。JavaScriptはself書くのが要らない代わりに、関数型な特徴を使うと、いちいちFunctionのcall(this, args...)メソッドが出てきます。本質的に要るものを1個減らすと、意図してないところにしわが1個増えるんですね。明解で端的な教育用言語Pythonに、そんないびつな"しわ"を寄せるような選択は、やるべきじゃないし、必要ですらない、と思います。

人気取りのためにJava人気に迎合せざるをえなかったJavaScriptは、かわいそうな兄弟なのかもしれません。

(余談)まあでも、ActionScript3までいくと、OOPの言語エンジンに関数リテラルとクロージャを不用意に設けて、クロージャのせいでガベゴレがクラッシュするなんていう、自ら逆方向の間違いを犯した自爆言語なので、もはや自業自得ですよね。AS3 ... 激しく動的でランタイムは弱いわ、型強制でコーディングは重いわ、おまけにFlashIDEはエディタに本当の型情報使わないわ、どこがいいんだろうね?

PythonOOP言語じゃない、だから、C++JavaRubyの人が知っている世界だけで評価すると間違う、それが確かに間違いにつながることは、JavaScriptがこんなにも示してくれているじゃないか、っていうお話でした。