2010年7月8日木曜日

新スタイルクラスの内部の話

原文:http://python-history.blogspot.com/2010/06/inside-story-on-new-style-classes.html
原文投稿者:Guido van Rossum

[注意:この投稿はとても長くて技術的です]

表面上は、新スタイルクラスは、元々のクラスの実装と非常に似通って見えるが、新スタイルクラスでは次のような数々の新しいコンセプトが導入されている。

  • __new__()という名前の低レベルのコンストラクタ
  • 属性アクセスのカスタマイズを一般的にできるようにするデスクリプタ
  • 静的メソッドとクラスメソッド
  • プロパティ(演算してから結果を返す属性)
  • デコレータ(Python 2.4から導入)
  • スロット
  • 新しいメソッド解決順序(Method Resolution Order, MRO)

このエントリーでは、これらのコンセプトについて光を当てていこうと思う。

__new__()という名前の低レベルのコンストラクタ

クラスを定義するときには通常の場合には、インスタンスの生成後にどのように新しいインスタンスを初期化するのかを定義する、__init__()メソッドを実装していた。しかし、クラスの作者が、インスタンスの生成方法そのものもカスタマイズしたいというケースがいくつかある。例えば、オブジェクトを永続化されたデータベースから復元する場合などである。"new"モジュールなど、普通ではない方法で特定の種類のオブジェクトを作成するためのライブラリはいくつかあったが、旧スタイルクラスではオブジェクトの生成をフックしてカスタマイズする方法が提供されていなかった。

新スタイルクラスでは、__new__()という新しいクラスメソッドが提供されたことで、クラスの作者が新しいクラスのインスタンスを生成する方法もカスタマイズできるようになった。__new__()メソッドをオーバーライドして、以前作成したインスタンスを返すことで、クラス作者はシングルトンパターンのようなパターンも実装することが可能である。また、他のクラスのインスタンスも返すことができる。しかし、__new__()の活用法には、これ以外にももっと重要なものがある。例えば、pickleモジュールでは、__new__はシリアライズされたオブジェクトを復元してインスタンスを作成するのに使用されている。このケースでは、インスタンスは生成されるが、__init__()メソッドは呼び出されないという実装になっている。

他にも、変更不可(immutable)型のサブクラス化を行う際にも、__new__が役に立つ。変更不可能という特性上、標準的な__init__()メソッドでは、このようなオブジェクトを初期化することはできない。そのため、オブジェクトの作成時に何か特別な初期化処理を行う必要がある。もし、変更不可能なオブジェクトの中に格納される値を変更したい場合には、ベースクラスの__new__メソッドに変更後の値を渡すことによって、このような処理を行うのに利用できる。

デスクリプタ

デスクリプタは、旧スタイルクラスの実装の中心となっている、束縛メソッドの概念を一般化したものである。旧式のクラスでは、インスタンス属性がインスタンス辞書の中から見つけられない場合には、クラス辞書を引き続き探索し、その後はベースクラスのクラス辞書を再帰的にたどっていくという振る舞いをする。もし属性がクラス辞書の中で見つかると、インスタンス辞書とは異なり、インタプリタが見つかったオブジェクトがPythonの関数オブジェクトであるかどうかがチェックされる。もし、そのオブジェクトが関数オブジェクトだった場合には、見つけたオブジェクトを返値にするのではなく、カリー化関数のように振る舞うラッパーオブジェクトを返す。ラッパーが呼ばれると、インスタンスを引数リストの先頭に挿入してから、オリジナルの関数が呼び出される。

例えば、クラスCのインスタンスxがあると想定しよう。今、このインスタンスに対して、x.f(0)という呼び出しが行われたとする。この操作を分解すると、まず、"f"という名前の属性をインスタンスxから探しだし、引数として0を渡して呼び出される。もし、"f"がクラスの中で定義されたメソッドと一致するのであれば、次の擬似コードの関数のような振る舞いをするラッパー関数を属性として返す:

def bound_f(arg):
    return f(x, arg)

もしも引数の0とともにこのラッパーが呼ばれると、このラッパー関数はx0という二つの引数をともなって"f"を呼び出す。これが、クラスのメソッドが"self"引数を取得する基本的なメカニズムである。

(ラップされていない)関数オブジェクトfにアクセスする別の方法としては、クラスCの属性として"f"という名前の属性の問い合わせをするというものがある。このような検索を行うと、ラッパーのつかない、関数fを単純に返す。言い換えると、x.f(0)は、C.f(x, 0)と同じということである。Pythonの中では、これらの呼び出しは基本的に等価である。

旧式のクラスでは、属性を調べて、他の種類のオブジェクトが見つかった場合には、ラッパーは生成されずに、クラス辞書の中から見つかったオブジェクトがそのまま返される。これにより、クラス属性をインスタンス変数の"デフォルト"値として利用することもできる。例えば、上記の例であれば、もしクラスCが"a"という属性名で数値の1を持っていて、xのインスタンス辞書に"a"というキーがなければ、x.a1となる。x.aに割り当てると、xのインスタンス辞書に"a"というキーができ、属性辞書の探索順序の影響でクラス属性の値が隠蔽される。x.aを削除すると、隠されていた値(1)に再びアクセスできるようになる。

残念ながら、何人かのPython開発者により、この実装の限界が発見されてしまった。限界の一つ目が、いくつかのメソッドPythonで実装し、他のメソッドをCで実装するという"ハイブリッド"クラスを実装することができないというものである。これはPythonの関数だけが上記のような方法でインスタンスにアクセスするためのメソッドを提供していたのと、この振る舞いが言語にハードコードされていたのが原因である。また、C++やJavaプログラマーが親しんでいる、静的メンバー関数のような異なる種類のメソッドを定義する方法もなかった。

この問題に対処するために、Python 2.2からは、上記のラッピングの振る舞いを素直に一般化した仕組みが導入された。Pythonの関数オブジェクトのみをラップするというハードコーディングされた振る舞いの代わりに、属性検索で見つかったオブジェクト(上記の例だと関数f)ごとにラッピングするようになった。もしオブジェクトが見つかると、__get__と呼ばれる特殊なメソッド名を持つ、「デスクリプタ」と呼ばれるオブジェクトが返される。次に__get__メソッドが呼ばれ、属性検索の結果として、このメソッドの返値が使用される。もしオブジェクトが__get__メソッドを持っていない場合には、それがそのまま返される。インスタンス属性検索コード内に特別な処理を行う関数オブジェクトを作らずに、関数オブジェクトをラッピングして返すという今まで通りの振る舞いにするために、関数オブジェクトには、以前のコードと同じようなラッパーを返す、__get__メソッドが追加された。このデフォルトのラッパー以外にも、ユーザが自由に__get__というメソッドを持つ他のクラスを定義し、インスタンス属性検索中でクラス辞書の中から発見された場合に、好きなようにラッピングすることもできるようになった。

属性検索コンセプトを一般化するのに加え、属性の設定と削除の操作にも、このアイディアを拡大して適用した。x.a = 1del x.aなど、今までと同じような割り当て操作を使用することができる。このような操作が行われた時に、"a"という属性がインスタンス辞書ではなく、インスタンスのクラス辞書の中で発見された時に、クラス辞書に保持されたオブジェクトに__set____delete__という特別メソッドがないかどうかチェックされる。(__del__というメソッドはまったく別の意味で既に使用されている。そのため、これらのメソッドを再定義することによって、デスクリプタオブジェクト属性の取得、設定、削除の操作の時にどのような処理が行われるかを、完全に制御することができる。しかし、デスクリプタインスタンスが、インスタンス辞書ではなく、クラス辞書内に設定された時にだけ適用されるという点は、重要なので最後にもう一度強調しておきたい。

staticmethod, classmethod, property

Python 2.2からは、classmethod, staticmethod, propertyという、新しいデスクリプタのメカニズムに関連する、3つのクラスが追加された。classmethodstaticmethodは、関数オブジェクトに関するシンプルなラッパで、保持している関数オブジェクトの呼び出し方が通常とは異なるラッパーを返す、__get__メソッドが実装されている。例えば、staticmethodのラッパーは、引数リストに変更をいっさい加えずに関数を呼び出す。classmethodのラッパーは、インスタンスそのものではなく、インスタンスのクラスを引数の先頭に追加してから関数を呼び出す。呼び出されるのがインスタンス経由であっても、クラス経由であっても、それぞれの引数は一緒となる。

propertyクラスは、"属性"に対する値の設定と、"属性"からの読み込みに関する対となる2つのメソッドを持つようなラッパーを生成する。例えば次のようなクラスがあったとする。

class C(object):
   def set_x(self, value):
       self.__x = value
   def get_x(self):
       return self.__x

propertyラッパーを使うと、属性"x"にアクセスされたときに、値の読み込みと設定に、ここで定義されたget_x, set_xメソッドが暗黙的に呼び出されるようにすることができる。

最初にclassmethod, staticmethod, propertyが導入されたときには、これらを簡単に扱える、特別な文法がなかった。そのときは、新しい文法(常に激しい議論が巻き起こる)と一緒に新しい機能を導入しようとすると、議論が収束しなくなって、導入できなくなると考えられたので、機能の追加だけが行われた。そのため、この機能を使用する場合には、通常通りクラスとメソッドを定義したあとに、メソッドをラップするための追加の文を追加する必要があった。

class C:
   def foo(cls, arg):
       ...
   foo = classmethod(foo)
   def bar(arg):
       ...
   bar = staticmethod(bar)

プロパティについても、同様の作法に従っていた。

class C:
 def set_x(self, value):
    self.__x = value
 def get_x(self):
    return self.__x
 x = property(get_x, set_x)
デコレータ

デスクリプタのやり方の不都合な点は、メソッド定義の最後まで読まないと、そのメソッドが静的メソッドが、クラスメソッドか、あるいはその他のユーザ定義の属性を持つメソッドか特定できないという点である。Python 2.4からは、最後に新しい文法が導入され、次のように書くことができるようになった。

class C:
 @classmethod
 def foo(cls, arg):
    ...
 @staticmethod
 def bar(arg):
    ...

@式 というのを、関数定義の前の行に書くことができるようになった。この構文を、デコレータと呼ぶ。__get__を実装したラッパーを作成するデスクリプタと混同しないようにして欲しい。デコレータ構文の文法(Javaのアノテーションから派生)に関しての議論は、「BDFL宣告」によって文法が決定されるまで、延々と続いた。(David BeazleyはBDFLと言う用語の歴史に関しては、私が書いたものとは別々に書いている)。

デコレータ機能は、言語の機能の中で、もっとも成功したものとなった。「うまくいけばここまで広がるだろう」と予想していた範囲いっぱいまで、幅広くカスタムデコレータが使用された。特にウェブフレームワークはこの文法の使用方法について、さまざまな発見をして応用された。この成功を受けて、Python 2.6からは、この文法は関数定義だけではなく、クラス定義でも使用できるように拡張された。

スロット

デスクリプタの導入によって可能になった別の拡張機能としては、クラスの__slots__属性がある。例えば、次のようにクラス宣言が行える

class C:
 __slots__ = ['x','y']
 ...

スロットが定義されると、いくつかのことが行われる。まず最初に、リストに定義されたのと同じ名前の属性しか、オブジェクトに定義できないように制限される。次に、属性が固定されると、それ以上はインスタンス辞書に属性を保持する必要がなくなるため、__dict__属性が削除される。ただし、ベースクラスにはそれがあり、__slots__を利用しないサブクラスで利用される。__dict__の代わりに、配列を使用して、予約された場所に属性が保存される。そのため、すべてのスロットの属性は、それぞれの属性が格納される配列のインデックスを知っている、デスクリプタのオブジェクトが割り当てられる。この機能の実装は、完全にC言語で構築されているため、とても効率が良い。

中には、__slots__を導入した目的が、属性名を制限することによるコードの安全性の向上であると誤解している人もいる。実際には、私の究極の目標はパフォーマンスであった。導入の動機としては、__slots__はデスクリプタの面白い応用例であったから、というのもあったが、これらの新スタイルクラスに導入された変更は、パフォーマンスに対して深刻な影響を与えるのを恐れていたからである。特に、データデスクリプタを適切に動作させるには、オブジェクトの属性に対するあらゆる操作をする前に、データデスクリプタの場合には、まずその属性があるかどうか、クラス辞書を先に見に行く必要がある。その場合には、手動でインスタンス辞書を操作する代わりに、デスクリプタが使用されて属性アクセスが行われる。しかし、このようなチェックを追加する場合には、インスタンス辞書にアクセスする前に、追加の検索が実行されるということを意味している。スロットを利用することでパフォーマンスを向上させることができるため、万が一、新スタイルクラスを導入して、そのようにパフォーマンスの劣化で失望した場合に利用することができる。後になって(パフォーマンスの劣化が思ったよりも少なくて)、不必要であることが分かったが、その時にはもう削除するには遅かった。もちろん、適切に使用すれば、スロットはパフォーマンスを実際に増加させられる。特に、小さいにオブジェクトが大量に作成される場面では、メモリの使用量の削減によって、大きくパフォーマンスは向上する。

次の投稿では、Pythonのメソッド解決順序(MRO)の歴史に触れてみたいと思う。

0 件のコメント:

コメントを投稿