Part 5: 自分自身のデータ型を作る

データ型とは?

データ型は、ある決まった一組の演算に従う特定のデータの集まりです。 例えば、整数は算術演算に従うビットの集まりであり、ベクトルは和、外積、規格化などの演算に従う3実数の集まりです。

Python では、新しいデータ型を定義することが許されており、組込み型に可能な事であれば何でもすることができます。 使用者から見れば差異はありません。 実際、このコースで使われるデータ型の多くは Python 中で定義されています: ベクトル、テキストファイル、内挿関数、などがそうです。 新しいデータ型を C 言語で定義することも可能です; Numeric モジュールの配列データ型がその一例です。

Python には、妥当である限り任意のデータ型によって使用可能な、一組の標準演算があります。算術演算、添字付け、関数呼び出し、などです。 さらに、データ型はメソッド (method) という形式で、任意の演算を提供することができます。 メソッドは関数に似ていますが、特定のデータオブジェクトに依存する点が異なります。 実はすでに多くの例を見てきました: append はリストに定義されたメソッド、close はファイルに定義されたメソッド、などです。

新しいデータ型を定義する理由はたくさんあります。 それによって、プログラムを読み易くができます -- 二つのベクトル ab について、(a+b)/2 と書く方が、 多くのプログラム言語が要求するように vector_scale(vector_add(a, b), 0.5) と書くよりもずっと明解です。 型定義によって、プログラムの異なる部分間の相互依存を小さくすることもできます。 ベクトルを使用するプログラムは、ベクトルがデータを格納する方法(リスト、タプル、配列、別々の三変数、のいずれも使えます)を知る必要はありません。 格納方法が何らかの理由で変更された場合でも、他のモジュールは影響を受けずに済む訳です。

オブジェクト指向プログラミング

ベクトルのような低級で汎用のデータ型から、応用が特定されたオブジェクトを表す高級のデータ型(例えば、分子、力場、波動関数など)まで、データ型を定義することのみによってプログラム全体を書くことも可能です。 この技法は、オブジェクト指向プログラミングとして知られています。 これは、(コードを関数とサブルーチンから構成する)伝統的な手続き型の流儀よりも一般に優れていることが経験的に分かっています。 プログラム全体中の異なる部分間がより独立しているために、理解し易く拡張と変更が容易なコードが得られるからです。 Python は、オブジェクト指向プログラミングで共通に使われる技術の大部分に対応しています。

大きなオブジェクト指向ソフトウエアシステムを書く際に最も難しいのは、使用するデータ型を決定する段階です。 経験則によれば、数学的なもの(配列や関数など)や、物理的なもの(分子や波動関数など)は、データ型で表すのに適しています。 しかし、もっと抽象的なデータ型、例えば汎用のデータ構造(リスト、スタックなど)またはアルゴリズムを表すデータ型、も同様に重要です。 オブジェクト指向デザインを学ぶには、経験を積むのが一番です;書き方が自明でない(すなわち、2〜3行以上の)プログラムを書く時は常に、オブジェクト指向に則って書いてみることを考えましょう。 また、この話題に関しては沢山の文献もあります。

クラス

新しいデータ型の定義は、クラスと呼ばれます。 クラスは、データ型の持つ全ての演算をメソッドとして定義します。 (算術演算のような標準的演算は、特別な名前を持つメソッドに割り当てられます。) 新しいオブジェクトの初期化も定義します。

次の例は、Vector クラスの定義の一部です。 ここに示すのは、初期化、加法、長さ計算だけであり、なかには簡単化されている(一般性を減じている)演算もあります。 完全な定義については、Scientific.Geometry モジュールのソースコードを見てください。

import Numeric

class Vector:

    def __init__(self, x, y, z):
        self.array = Numeric.array([x,y,z])

    def __add__(self, other):
	sum = self.array+other.array
	return Vector(sum[0], sum[1], sum[2])

    def length(self):
	return Numeric.sqrt(Numeric.add.reduce(self.array*self.array))

メソッドは関数の様に定義され、しかも振る舞いも関数と殆ど同様です。 ただし、メソッドの第一引数は特別な意味を持っています:それが表すのは、そのメソッドが呼び出される対象となるオブジェクトです。 この引数は習慣的に self と書かれますが、代りにどんな名前を使っても構いません。 v.length()v はベクトルとします)というメソッド呼び出しの際には、変数 selfv の値を受取ります。

名前の始めと終りが二重下線のメソッドは、特殊な意味を持ちます; それらは普通は明示的には呼び出されません(可能ではありますが)。 最も重要な特殊メソッドは __init__ で、オブジェクトが生成された直後に呼ばれます。 Vector(1., 0., 1.) と書くと、それは新しいベクトルオブジェクトを生成し、次にそのメソッドである __init__ を呼び、その新しいオブジェクトの局所変数に割当てられた配列に3つの座標を格納します。

算術演算も、特殊メソッドとして使うようになっています。 a+b という表現は、a.__add__(b) と等価ですし、他の演算も類似の等価表現を持ちます;詳細は、 Python Language Reference を参照して下さい。

まだ他にも、添字付け、コピー、印字などに関する特殊メソッドがあります。 意味のあるメソッドのみを作るべきですし、それもデフォルトの振舞いが十分でない場合だけにするべきです。 例えばベクトルについては、座標値を印字する記法を定義するのは有意義です。 これは、特殊メソッドを一つ追加すればできます:

    def __repr__(self):
        return 'Vector(%s,%s,%s)' % (`self.array[0]`,
    				     `self.array[1]`,`self.array[2]`)

属性

Python のオブジェクトは、任意の数の属性を持てます。 属性は変数とよく似ていますが、属性が特定のオブジェクトに付随するのに対し、変数はモジュールあるいは関数に付随する点が異なります。 実際、モジュール中で定義される変数は、モジュールオブジェクトの属性に他ならないのです。 属性にアクセスするときには、object.attribute という記法をいつも用います。 メソッド名も属性です。これは関数名が変数であることと同様です。

他の多くのオブジェクト指向言語とは違って、Python には属性へのアクセス制限がありません。 どんなコードでも、任意のオブジェクト中のあらゆる属性を使用し、変更することさえも可能です。 例えば、

import Numeric
Numeric.sqrt = Numeric.exp
を実行して、sqrtexp のように振る舞うようにすることもできます。 明らかにこれは良い考えではありません。しかし、Python はあなた自身の愚かさからあなたを保護してくれようとはしません。 もちろん、たまたま間違って属性を変更してしまう可能性もいくらかはありますが、実際上は問題にはなりません。

"全てがオブジェクト":Python の世界

Python は、とても一貫性のある言語です。 その全体像は、オブジェクト、名前、名前空間だけから成っています。 全てのデータはオブジェクトに保管されます。ただし、モジュール、関数、クラス、メソッドもオブジェクトです。 オブジェクトには名前を付与することができ、名前は名前空間中に存在しています。 あらゆるオブジェクトには、その属性用の名前空間が附随しています。 さらに、関数とメソッドは、(局所変数用の)一時的な名前空間を、その実行中に持ちます。

ある名前がどの名前空間に置かれるかについての規則が2、3あります。 モジュール中で定義されたものは、モジュールオブジェクトの属性名前空間に置かれます。 関数とメソッド中の定義は、一時的な実行名前空間に置かれますが、関数中のコードはそれを取り囲むモジュール中の名前も使うことができます(ただしそれを割り付けることはできません)。 クラス中の定義は、そのクラスの属性名前空間に置かれます。 最後に、クラスから作られたオブジェクト(クラスインスタンス)は、当然ながら自身の属性名前空間を持ち、あらゆる割当はその中でなされます。 しかし、この名前空間に無い属性が要求された時は、クラス名前空間が検索されます;メソッドは通常このようにして見つけられます。

クラスの特殊化と拡張

複数のデータ型が、何らかの共通性を持つということは多くあります。 あるデータ型が他の型の特殊化されたものである場合もあるでしょう; 例えば、特殊なベクトルとして、規格化されたベクトルを導入することもできます。 あるいは、いくつかのデータ型が多くの演算を共有し、しかしある側面では異なっているという場合もあり得ます。 例えば、スカラー場とベクトル場を表すデータ型を定義することができます。 それらは共通の性質(例えば、格子点上で定義されること)を持つ一方で、スカラー場には「勾配」、ベクトル場には「発散」という様に、各々が特定の演算を持ちます。 両者のデータ型は、共通の動作を定義しているけれどもプログラム中で直接は使用されない様な、「場」というデータ型の特殊化されたものとして作ることができます。 (これは、抽象クラスと呼ばれることがあります。)

特殊化の技法は継承と呼ばれます。 あるクラスが、他のクラスからメソッドを継承し、変更を要するものは置き換え、自身のメソッドを追加する、ということが可能です。 これの主な利点は、コードの重複を避けることにあります。 重複は間違いの主因ですし、もちろんメモリの無駄でもあります。

次のコードは、空間内における方向、すなわち長さ1のベクトル、を表すクラスを、上で定義したベクトルクラスに基づいて定義します:

class Direction(Vector):

    def __init__(self, x, y, z):
        Vector.__init__(self, x, y, z)
        self.array = self.array/self.length()
再定義されているメソッドは初期化だけで、それがベクトルを規格化しています。 この初期化メソッドが、まず Vector クラスの初期化メソッドを呼んでから規格化を行っていることに注意して下さい。

この Direction メソッドは、Vector から全ての演算を継承しています。 これは、新しいクラスにおいて後者のコードが繰り返されているかの様に振る舞います。 特に、二つの "方向" の和は、別の規格化された "方向" ではなく、単なるベクトルになります。 この場合に規格化された結果を得るためには、__add__ メソッドを同様に定義し直さなくてはならないでしょう。 しかし、方向の加法はそれ程有用な演算では無いので、これは労力に値しないかも知れません。

エラー処理

エラーが起こった時には、Python はスタックトレース(エラーが起こった時に実行されていた全関数のリスト)を印字して停止します。 これは多くの場合に有用ですが、常にそうとは限りません。 エラーを自分自身で処理したいこともあるでしょう。 例えば、警告を印字したり、利用者による入力を促したり、別の計算を行なったりです。 Python では、任意のコードから個々のエラー状況を捕獲できますし、どんな方法ででも必要に応じてそれを処理することができます。

エラーの型を指定するために、Python はいくつかの組込みエラーオブジェクトを持っています。 例えば、ValueError(値が演算に対して不適。例えば平方根に対する負の数)あるいは TypeError(データ型が不適。例えば、文字列の対数を求めたりした場合)などです。 プログラムは特定のエラーオブジェクトやコレクション、または任意のエラーを捕獲できます。 エラーの捕獲の一般形は次の様になります。

try:
    x = someFunction(a)
    anotherFunction(x)
except ValueError:
    print "Something's wrong"
else:
    print "No error"
まず最初に try: 以下のコードが実行されます。 ValueError が発生した場合には except ValueError: 以下のコード、それ以外の場合には else: 以下のコードが実行されます;ただし後者は無くてもかまいません。 複数のエラー型を捕獲するにはタプルを使います(例えば except (ValueError, TypeError))。 エラーを全て捕獲するには、単にexcept: とします。 複数のエラー型を個別に処理するには、except ...: code を繰り返し使います。 より詳しい説明と可能性については、 language reference manual を参照して下さい。

もちろん Python プログラムはエラーを発生させることもできます。 それには、raise ErrorObject を使います。または、利用者のために説明を付け加えるときには、raise ErrorObject, "Explanation" とします。 ErrorObject としては、あらかじめ定義されたエラーオブジェクトの何れもが使えますし、文字列も使えます。 多くのモジュールは、自身のエラー型を文字列で定義し、他のモジュールにそれらを import させます:

# モジュール A

AError = "Error in module A"

def someFunction(x):
    raise AError
# モジュール B

import A

try:
    A.someFunction(0)
except A.AError:
    print "Something went wrong"

練習問題


目次