Pythonのselfはなぜ必要かをJavaScriptのthisで考える
あなたがもし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")
PythonはOOPするとき、「関数」だけで明確に説明できる仕様だし、素直に書いたものが最も問題が少なくなる。そのうえ、関数の移動や変数の改名など、機械的なパターン作業でリファクタリングできる可能性が非常に高い。引数の位置がずれたり、関数の呼び出しを、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はエディタに本当の型情報使わないわ、どこがいいんだろうね?
PythonはOOP言語じゃない、だから、C++とJavaとRubyの人が知っている世界だけで評価すると間違う、それが確かに間違いにつながることは、JavaScriptがこんなにも示してくれているじゃないか、っていうお話でした。