原文:http://python-history.blogspot.com/2009/02/adding-support-for-user-defined-classes.html
原文投稿者:Guido van Rossum
にわかには信じられない人もいると思うが、CWIで開発が行われていた最初の一年の間、Pythonはクラスをサポートしておらず、最初の公開リリースの前にオブジェクト指向をサポートするようになった。どのようにクラスが追加されたかという過去の経緯を理解してもらう手助けになると思うので、現在のPythonがどのようにクラスをサポートしているのか、という点から話を始めようと思う。
Pythonはオーソドックスなスタックベースのバイトコードインタプリタ、もしくは仮想マシンとしてC言語で実装されている。プリミティブな型も同様にC言語で実装されている。アーキテクチャとしては一貫して「オブジェクト」を使用しているが、C言語はこのオブジェクトを直接サポートする仕組みを持っていない。そのため、構造体と関数ポインタを使用してこれらの仕組みが実装されている。Pythonの仮想マシンでは、すべてのオブジェクトの型がなるべく実装すべき演算子と、絶対に実装すべき演算子が何十個か定義されている。例えば「属性の取得」、「足し算を行う」、「関数の呼び出し」などである。オブジェクトの型は静的に確保された構造体として表現される。この構造体には標準的な演算子に関する関数のポインタが一式格納されている。ほとんどの場合は、これらの関数は静的な関数への参照で初期化される。いくつかオプションの演算子があるが、この関数ポインタをNULLのままにしておくことで、これを実装しないでおくことが可能である。実装されていないオプションを使った演算を行ってしまうと、実行時にエラーを生成するか、場合によってはデフォルトで実装されている演算子が代わりに使用される。Pythonの型の構造体にはさまざまなデータフィールドを格納することができ、このクラス固有の追加のメソッドへの参照も格納することもできる。この場合は、文字列(メソッド名)と関数ポインタ(実装)の配列として構造体の中に格納される。Pythonではイントロスペクションを行えるが、これは、実行時には他の要素と同様に、型構造体そのものもオブジェクトとして使用することができるようになっていることで実現されているのである。
実装における重要な側面は、これらの機能の実装がすべて、C言語を中心に実装されているということである。実際、標準の演算子やメソッドはすべてC言語の関数で使って実装されている。もともと、バイトコードインタプリタはピュアPythonの関数と、C言語で作成されたメソッドや関数を呼び出す機能しかサポートしていないのである。私の記憶が正しければ、PythonでもC++のようなクラス定義を追加して、Pythonでオブジェクトを作成する機能を追加したほうがいい、とアドバイスをしてくれたのは、私の同僚だったSiebren van der Zee氏が最初であった。
ユーザ定義のオブジェクトを実装するにあたり、私はなるべくシンプルな設計をとった。「クラスオブジェクト」への参照ポインタと、「インスタンス辞書」と呼ばれる辞書が格納できる、新しい種類の組み込みオブジェクトを用意した。これを使用することで、ユーザ定義のオブジェクトを表現するという仕組みになった。クラスオブジェクトは同じクラスに属するすべてのインスタンスで共有される。インスタンス辞書はインスタンス変数を格納する。
この実装では、それぞれのオブジェクトごとのインスタンス変数はインスタンス辞書に格納され、同じクラスのすべてのインスタンスで共有する要素や、特にメソッドは、クラスオブジェクトに格納される。クラスオブジェクトの実装においても、できるだけシンプルな実装を選択した。クラスのメソッドは、メソッド名をキーとする辞書に格納された。私はこれをクラス辞書と呼んでいた。継承をサポートするためにクラスオブジェクトには、ベースクラスに関連するクラスオブジェクト群への参照を追加した。その当時、私はあまりクラスには詳しくなかったが、その当時のC++が多重継承をサポートしたことについては知っていた。私は、継承をサポートするからには、単純なバージョンの多重継承をサポートした方がいいと考えた。このようにして、すべてのクラスオブジェクトはひとつ以上のベースクラスを持つことができるようになった。
このPythonのクラスの実装では、オブジェクトの裏側で動作しているメカニズムはとてもシンプルになっている。インスタンス変数やクラス変数が変更されると、裏側にある辞書オブジェクトに変更が反映されるという仕組みになっている。例えば、インスタンス上にインスタンス変数に値を設定すると、そのオブジェクトが持っている、ローカルのインスタンス辞書が更新されるのである。同様に、オブジェクトのインスタンス変数の値を見にいくときは、変数がその中に定義されているかどうか、そのオブジェクトのインスタンス辞書をただチェックするだけである。もし変数がその辞書の中にないとすると、少しだけ面白いことが発生する。この場合は、インスタンス辞書の次にクラス辞書の中の探索が行われ、それでも見つからなければ、ベースクラスのクラス辞書が順番に探索されていく。このようなクラスオブジェクトとベースクラスの中を属性探索していくプロセスは、メソッドの場所を探索するのとほぼ一緒である。少し前に説明したように、同じクラスに属すすべてのインスタンスが共有しているクラスオブジェクトがあり、その辞書の中にメソッドが格納されている。メソッドが必要なときには、当然それぞれのオブジェクトが持つインスタンス辞書の中は探索しないだろう。その代わりに、まずクラス辞書の中を調べ、その後はメソッドが見つかって止まるまで、順番にベースクラスを見ていく必要がある。それぞれのベースクラスでは、同じ再帰的なアルゴリズムが実装されている。これは深さ優先、左から右へのルールと呼ばれている。これがほとんどのPythonのバージョンで用いられているデフォルトのメソッド解決順序(MRO)である。もっと新しい現在のリリースではより洗練されたMROを採用しているが、これについてはこれについては別の機会に話をしたいと思う。
クラスを実装する際に私が目標の一つとしていたのは、シンプルさを維持する、ということである。そのため、Pythonはメソッドを定義する時には事前のエラーチェックや整合性のチェックは行うことはない。もし、あるクラスが、ベースクラスで定義されているメソッドをオーバーライドした場合、再定義されたメソッドの引数の数は同一であるか、もしくは、オリジナルのベースクラスのメソッドと同じ呼び方ができるか、などのチェックが行われることはない。上記のメソッド解決順序のアルゴリズムは、ユーザが指定した引数がどんなものであっても、最初に見つけたメソッドを返し、その引数を渡してメソッドを呼び出すだけである。
このような設計を行ったが、この仕様の想定外の使われ方をしている機能も他にいくつか存在する。例えば、クラス辞書は当初、メソッドを置く場所として想定していたが、メソッドと同じように他の種類のオブジェクトを格納してはいけないという理由もなかった。そのため、数値や文字列といったオブジェクトをクラス辞書の中に定義したとすると、これらはクラス変数と呼ばれるものになるのである。通常の変数はそれぞれのインスタンスごとにデータを格納するが、これとは異なり、クラス変数はそのクラスの全インスタンス間で共有される変数となるのである。
Pythonのクラスの実装はシンプルであるが、驚くほどの柔軟さを備えている。実際に、実行時に簡単に中の情報を調べることができるという「ファーストクラスオブジェクト」だけを作成するような実装にはなっていないが、クラスを動的に変更することも可能である。例えば、クラスオブジェクトが作成された後であっても、クラス辞書を更新するだけで、メソッドを追加したり、変更したりすることができる。(*)Pythonの動的な性質が意味するところは、クラスのすべてのインスタンス、クラスのすべてのインスタンスそのサブクラスという単位で、すぐにこれらの変更が引き起こされるのである。同様に、それぞれのオブジェクトもまた、動的に追加したり、変更したり、インスタンス変数を削除することができる。Pythonのオブジェクトの実装が完了した後に知ったのだが、Smalltalkでは属性の追加はオブジェクト作成時に定義する時にのみ限定されているため、Pythonの実装はSmalltalkよりも自由である。
classシンタックスの作成ユーザ定義のクラスやインスタンスの実行時の表現が設計できたので、次のタスクは、クラス定義の文法を設計することである。具体的に言うと、メソッド定義もこれに含まれる。私が関数のシンタックスとは異なるメソッド用のシンタックスを追加したくない、と考えていたことが設計上の主な制約であった。似ているが微妙に異なるようなケースを扱うために文法とバイトコードジェネレータをリファクタリングするというのは、とても大きな仕事のように感じていたからである。しかし、例えば文法を同じにすることに成功したとしても、その次にはインスタンス変数を取り扱う方法を考え出す必要がある。当初は、C++のような、暗黙のインスタンス変数をエミュレートできないかと期待した。例えば、C++では、以下のようなコードでクラスを定義する。
class A { public: int x; void spam(int y) { printf("%d %d\n", x, y); } };
このクラスではインスタンス変数xが定義されている。メソッドの中では暗黙的に、該当するインスタンス変数を参照しに行くことになる。例えば、spam()メソッドの中では、変数xは、関数のパラメータとして定義されたわけでも、ローカル変数として定義されているわけでもないため、クラスはxという名前を持つインスタンス変数であると断定するのである。そこで初めてxへの参照が、インスタンス変数と結びつけられるのである。私は当初は同じようなやり方をPythonの中でも提供できれば、と期待していた。しかしすぐに、変数宣言がない言語においては、インスタンス変数とローカル変数をエレガントに識別する方法が存在しないということが分かった。
理論的にはインスタンス変数の値を取得するの難しくはない。Pythonは既に、ローカル、グローバル、組み込みと、宣言のない変数名から順番に探索する機能を備えているからである。これらはすべて、名前をキーにした辞書にマッピングされて実装されている。変数参照ごとに、結果が見つかるまで、これらの辞書を順番に探索する。例えば、ローカル変数pと、グローバル変数qを持つ関数を実行していたとする。例えば、"print p, q"という文を実行する場合は、pはローカルなので、検索順序の最初にある、ローカル変数を格納している辞書を探索しただけで見つかる。変数qは最初の辞書の中には見つからず、グローバル変数を格納している2番目の辞書の中で見つかることになる。
メソッドの実行時に、メソッド検索リストの先頭に現在のオブジェクトのインスタンス辞書を追加するのは簡単な作業である。インスタンス変数xと、ローカル変数yを使用した、オブジェクトのメソッドの中で、"print x, y"があれば、検索過程の1番目にあるインスタンス辞書から変数xを見つけることは可能である。変数yは2番目の辞書であるローカル変数辞書の中にある。
この戦略の問題は、これがインスタンス変数の設定をぼろぼろにしてしまうというところにある。Pythonの変数割り当てでは、これらの辞書の中の変数名まで見にいくことはせずに、検索順の最初の辞書内の要素(通常はローカル変数)に関して単純に変数を追加したり変更したりしている。この仕組みがあるので、変数を定義すると、デフォルトではローカルスコープ内に作成されることになる。Pythonには「グローバル宣言」というものがあるが、これは関数や変数ごとのこの基本の動作を上書きして、グローバルスコープに変数を作成するために必要となる。
割り当てに関するこの単純なやり方をそのままにして、インスタンス辞書を検索順序の最初の順序においてしまうと、メソッド内でローカル変数を割り当てるのが不可能になってしまうことになる。以下のようなメソッドがあったとする。
def spam(y): x = 1 y = 2
変数xとyの割り当ては、xに関してはインスタンス変数xの定義の上書きになり、ローカル変数のコピーとなるインスタンス変数yが作成されることになる。インスタンス変数とローカル変数の検索順序を交換したとすると、単に問題がひっくり返るだけである。結果としてインスタンス変数に割り当てることが不可能になるのである。
インスタンス変数がすでに存在している場合のインスタンス変数への割り当て方を変更すると、今度はローカル変数への操作がうまく行かなくなってしまい、結果としてブートストラップ問題になってしまう。どのようにしてインスタンス変数を初期化時に作成すればいいのだろうか?可能な解決策の一つとして、グローバル変数と同様に、インスタンス変数についても明示的に宣言しなければならない、とすることもできるだろう。しかし、こうしてしまうと、すべての変数の宣言が必要となってしまうため、私はこれを選択したいとは思わなかった。加えて、グローバル変数であると示すためには仕様を追加する必要があったが、グローバル変数に限って言えば、使う場面はほとんど特殊なケースのみで、ほとんどのコードでは慎重に使われている、という事情もある。インスタンス変数のための特別な仕様が必要ということになると、このグローバル変数の状況とは異なり、クラス内のあらゆるところで使用されることが前提となる。もう一つとりうる解決策としては、文法的にインスタンス変数とローカル変数の区別をきちんと行うようにする、というものもある。例えば、Rubyが現在取っているアプローチのように、すべてのインスタンス変数の名前を、@のような特殊文字で始まるようにしてしまうというものである。あるいは、特殊な命名規則をつけるとか、大文字小文字のルールを決めるというのもあるだろう。しかし、私はこれらの選択肢はどれも魅力的には感じなかったし、今も感じていない。
最終的に、私はインスタンス変数への参照を曖昧なままにするというアイディアをあきらめることにした。C++のような言語はthis->fooと明示的にインスタンス変数fooへの参照を書くことが可能である。このようにして書くことで、同名のローカル変数fooと区別することができるのである。そうして、私はインスタンス変数への参照方法を、このような明示的なやり方に限定することにした。加えて、私は現在のオブジェクトを示す、"this"のような特殊なキーワードを作成する代わりに、"this"あるいはそれと同等なものを、メソッドの最初の引数の名前から取ることにした。こうして、インスタンス変数はこの引数の属性として参照できるようになった。
このように明示的な参照をするようにしたため、メソッドの定義のために特別な文法を作成する必要もなくなったし、変数の検索のための複雑な仕組みについて悩む必要もなくなった。実際には、プログラマは、単純に最初の引数として、インスタンスが入る引数をつけて関数を作成するだけである。Pythonでは、この変数の名前として、"self"を慣例的に使用している。サンプルコードは以下のようになる。
def spam(self,y): print self.x, y
この方法は、私がModula-3で見た何かと似ている。私は既にModula-3のimportと例外ハンドリングの文法を参考にさせてもらっている。Modula-3はクラスは持っていないが、関数ポインタをメンバーとして持っているレコード型を作成することが可能である。関数ポインタメンバーは近くに定義されている関数であるかのように初期化される。メンバーの呼び出しへのシンタックスシュガーも追加される。例えば、xをレコードの変数、mを関数fに初期化されているレコードの関数ポインタメンバーであったとすると、x.m(args)の呼び出しは、f(x, args)と同等になる。この仕様は、典型的なオブジェクトとメソッドの実装と同じであり、Python同様に、最初の引数の属性と、インスタンス変数を同一視することが可能になる。
Pythonのクラスの文法の残りの部分は、これまで説明した仕様に従って決定されたり、他の実装によって課せられた制約によって決定された。class文も、シンプルにするという私の願望を維持するため一連のメソッド定義からなると考えたが、この慣習とは異なる実装になった。最初の引数が"self"という名前にする必要があるが、関数の定義とメソッドの定義は瓜二つな実装とした。それに加えて、特殊な種類のクラスのメソッド(コンストラクタやデストラクタなど)については、新しい仕様を考え出す代わりに、これらの機能はユーザが単純に特殊な名前(__init__や、__del__など)でメソッドを実装することで扱えるようにした。C言語の、アンダースコアから始まる識別子の名前はコンパイラの予約語であり、例えばC言語のプリプロセッサで展開される__FILE__などのように特殊な意味を持っていることが多いが、Pythonの特殊なメソッドの名前の規則はそのC言語を元にしている。
この結果、私が考えていたクラス定義のコードは以下のようなものになった。
class A: def __init__(self,x): self.x = x def spam(self,y): print self.x, y
何度も繰り返すが、私は、事前に作成したコードを、なるべく多く再利用したいと思っていた。Pythonにおける関数定義は、関数オブジェクトへの参照を、現在の名前空間内の中の変数にセットする実行文である。このときの変数名は関数名が利用される。そして、クラスの扱いについて違うアプローチが思い浮かんだが、単純に新しい名前空間の中で実行される一連の文としてクラスの本体を解釈するのが良いと考えた。この新しい名前空間は保存され、クラス辞書を初期化してクラスオブジェクトを作成するのに使用される。採用された実装方法は、まずクラスの本体を無名関数に入れ、この無名関数はクラスの本体の中のすべての文を実行し、ローカル変数の辞書を結果として返す。この辞書はクラスオブジェクトを作成するヘルパー関数に渡される。最後に、このクラスオブジェクトは現在のスコープの中の変数に格納される。この時、クラス名が変数名として使用される。無名関数として実行されるため、クラスの本体の宣言中に文法的に正しいPythonの実行文は何でも入れられるのであるが、これを知って驚くユーザは多い。この機能は、使いやすさを犠牲にしない範囲で文法をなるべくシンプルにしたいという私の希望を率直に拡張した結果生まれたものである。
最後に、クラスのインスタンス作成の文法について説明したいと思う。C++やJavaのような多くの言語は、新しいクラスのインスタンスを作成するのに、"new"といった特殊な演算子を使用する。C++のパーサの中ではクラス名はどちらかというと、特殊な状態として扱われるため、このようなことが可能になっている。Pythonの場合はクラスも単に変数に格納されたクラスオブジェクトでしかないため、特別扱いされることはない。Pythonのパーサは、関数呼び出しがあったとしても、どのような種類のオブジェクトかということに関しては無関心なので、私はすぐにクラスオブジェクトを呼び出し可能なものとして作成するのが良いと思った。既にある関数呼び出しという機能を利用することで、新しい文法が不要となり、最小限の手間で実現できるからである。。頭の中の時間を今日まで戻すと、最近はインスタンス生成のパターンとしてファクトリー関数というものが好んで頻繁に使用される。私がPythonに組み込んできた機能はまさに、クラスをそれぞれのファクトリー関数に転換するというものであった。
特殊メソッドクラスの実装に関連する最後のセクションとして、私が主な目標としている、クラス実装をシンプルにするということに関して簡単に触れたいと思う。ほとんどのオブジェクト指向言語には、クラスに対してのみ適用されるような、特殊な演算子とメソッドが存在する。例えば、C++ではコンストラクタとデストラクタ専用の特殊な文法が存在し、通常の関数やメソッドを定義するために使用される通常の文法と異なっている。
私はオブジェクトが特殊な演算子を扱うのに、特殊な文法は導入したくないと思っていた。そのため、「特殊メソッド」として、__init__や__del__のような名前を事前に設定して、特殊な演算子とこの名前をマッピングすることによって扱えるようにした。これらの名前を定義することによって、ユーザはオブジェクトの構築と破棄に関連するコードが実装できるようになったのである。
私はこのテクニックをユーザのクラスに適用することによって、Pythonの演算子を再定義できるようにした。前に触れたとおり、PythonではC言語を使い、組み込みオブジェクトの機能(例えば、「属性の追加」、「足し算を行う」、「関数の呼び出し」など)を実装するために関数ポインタのテーブルを用いている。これらの機能をユーザ定義クラスの中で定義するために、様々な関数ポインタと、__getattr__, __add__, __call__といった特殊なメソッド名のマッピングをおこなった。新しいPythonオブジェクトをC言語で実装するときには、これらの名前と関数ポインタのテーブルの間の関連を定義する必要があるのである。
(*) 最終的に、新スタイルクラスはクラスの__dict__の変更を制御するためにこれが必要であった。現在も動的に変更することはできるが、クラスの__dict__を直接操作するのではなく、属性割り当てを使用すべきである。
0 件のコメント:
コメントを投稿