7. オブジェクト指向言語としてのPython

7.1. オブジェクト指向プログラミング (OOP) としての Python

Python はオブジェクト指向プログラミング (Object Oriented Programming: OOP) をサポートしています. OOP の目指すべきゴールは

  • データ構造と,その構造を取り扱うコードを一つのクラスにまとめること. (カプセル化)

  • 元になるクラスを拡張することを簡便化すること (継承)

  • クラス(または関数(メソッド))に与えられるデータによって, 適切な挙動を取らせること (多態性)

を言語機能として持っていて,これらの機能を用いることで,

  • 似たような文脈でコードを再利用し

  • できるだけ楽にコードを書くこと

です.(批判もありますが建前上は)

ここでは,それほど複雑でない事例について取り扱い,簡単な説明を行います. 興味がある人はチュートリアルなどで勉強しましょう.

7.1.1. オブジェクトの定義と呼び出し

ここでは簡単な例を用いて説明します. ありがちな例ですが,2次元上の座標点表すクラスを Point2D として設計してみます. 2次元上の座標点なので,クラス内部には変数として xy を持つものとします. このようなクラス内部の変数は 属性(attribute) などと呼ばれます.

このような場合の python のコード例は 下記のようになります.

1class Point2D:
2   def __init__(self):
3      self.x = 0.0
4      self.y = 0.0

ここでは class から始まるキーワードでブロックが開始されており,このブロック内部がクラスの 定義となります.クラス内部に def キーワードで始まる部分がありますが,これは クラス内部での関数となります.この場合 Point2D は,関数 __init__() を持っていると 読み解けます.

さらに謎の変数 self があります. これは, python を用いてクラス定義する場合に必須となる表現で,変数 self はオブジェクト自身を指す変数です. python のオブジェクト指向表現では,この self を介してオブジェクト内部の変数や関数にアクセスしていくことに なり,クラス内部で定義される関数(メソッド)の第1引数は必ず self を指定する必要があります. (呼び出すときに表記する必要がないので,やや違和感があるかもしれませんが,そういうものだと割り切ってください)

またクラス内の関数としてアンダースコア2つでくくられた関数は特殊な意味を持つ関数で, __init()__() は, コンストラクタと呼ばれる関数をさし,オブジェクトが生成されるときに呼び出される関数となります. この __init()__() コンストラクタでは,クラス内変数 xy に値 0.0 を代入していることが 読み取れます.

それでは,このクラスを使って実際に使ってみます.

1In [1]: class Point2D:
2   ...:   def __init__(self):
3   ...:     self.x = 0.0
4   ...:     self.y = 0.0
5   ...:
6In [2]: p = Point2D()  # インスタンス化

クラスを関数のように呼び出す p = Point2D() ことによって, オブジェクトが生成されます.インスタンス化された変数を print()type() で確認すると

1In [1]: print(p)
2Out[1]: <__main__.Point2D object at 0xffffa31828f0>
3In [2]: type(p)
4Out[2]: <class '__main__.Point2D'>

のようにオブジェクトですよという表示になります. 上記,インスタンス p の属性(メンバ変数)は, ピリオド . を使ってアクセスできます.

1In [3]: p.x
2Out[3]: 0.0

なお,属性値は,クラス外部から参照できるだけでなく,代入操作も可能となっています. (基本的に python の属性値はパブリック変数扱い)

1In [4]: print(p.y)
2Out[4]: 0.0
3In [5]: p.y = 3.0  # 代入操作
4In [6]: print(p.y)
5Out[6]: 3.0

なお,コンストラクタの引数にデフォルト値付きの引数を用意しておいてやると, 値を最初から設定した形で呼び出しが可能になり便利です.

 1In [1]: class Point2D:
 2   ...:    def __init__(self, x=0.0, y=0.0):
 3   ...:        self.x = x
 4   ...:         self.y = y
 5   ...:
 6In [2]: p = Point2D()
 7In [3]: print(p.x)
 8Out[3]: 0.0
 9In [4]: q = Point2D(1.0, 2.0)  # 初期値指定をしてインスタンス化
10In [5]: print(q.y)
11Out[5]: 2.0

7.1.2. メソッド

オブジェクト指向言語において,クラスは大雑把に言えば,データとデータを取り扱うためのメソッドから成り立っています. python におけるオブジェクトでは, self というキーワードが,クラス内のデータやメソッドであるという識別子に なります.

ここでは, Point2D にいくつかのメソッドを付け足して,2次元上の点を扱うためのメソッドを定義してみましょう.

  • 原点からの距離(L2ノルム)を求める (L2norm())

  • 他の点とのベクトル和を求める(add())

 1import math # 平方根を用いるので数学パッケージを入れておく
 2
 3class Point2D:
 4   def __init__(self, x=0.0, y=0.0):
 5      self.x, self.y = x, y
 6
 7   def L2norm(self):
 8      return math.sqrt(self.x**2 + self.y**2)
 9
10   def add(self, other):
11      return Point2D(self.x + other.x, self.y + other.y)

で設定しておいて

1In [1]: p = Point2D(1., 2.)
2In [2]: q = Point2D(3., 4.)
3In [3]: p.L2norm()
4Out[3]: 2.23606797749979
5In [4]: r = p.add(q)
6In [5]: r.x
7Out[5]: 4.0

7.1.2.1. 特殊メソッドについて

上記の class:Point2Dadd() の使用に関して, p.add(q) という表記は, 使いにくい(し,読みにくい)と思った人がほとんどじゃないかと思います.

1r = p.add(q) # とかくより
2r = p + q    # のようにかけると読みやすい

上の足し算を用いてかければ良いのに,という人に対して, python 側では用意しているのが特殊メソッドと呼ばれる関数たちを用意しています. 前述の __init__() は,コンストラクタと呼ばれる特殊メソッドでしたが, これの他にもいろいろあります.例えば上述の add()__add__() と名称変更してみます.

 1import math # 平方根を用いるので数学パッケージを入れておく
 2
 3class Point2D:
 4   def __init__(self, x=0.0, y=0.0):
 5      self.x, self.y = x, y
 6
 7   def L2norm(self):
 8      return math.sqrt(self.x**2 + self.y**2)
 9
10   def __add__(self, other): # <- 名前が変更された
11      return Point2D(self.x + other.x, self.y + other.y)

この変更を行って,Point2D クラスで定義したインスタンス間の + 演算を 書くと __add__() が呼び出されるので

1In [1]: p = Point2D(1, 2)
2In [2]: q = Point2D(3, 4)
3In [3]: r = p + q  # <-- この書き方ができるようになる
4In [4]: r.x
5Out[4]: 4

のように表記できるようになります.このような特定の演算子にマッピングされるメソッドを特殊メソッドと呼びます.

このほかにも,リストなどのコンテナを内包しておいて

  • __len__(self)():

    コンテナの要素数を返す

  • __getitem__(self, key)():

    key で指定されるコンテナのスライスを返す

関数を用意すると,繰り返しを扱うイテレータと呼ばれるクラスを作り出すことができます.

1class Foo():
2   def __init__(self, lst: List):
3       self.lst = lst
4
5   def __len__(self) -> int:
6       return len(self.lst)
7
8   def __getitem__(self, key) -> "Foo":
9       return self.lst[key]

のような定義をしておき

1In [1]: my_list = [1, 2, 3, 4, 5]
2In [2]: container = Foo(my_list)
3In [3]: len(container)
4Out[3]: 5
5In [3]: container.lst[1:3]    # <- 通常のアクセス.クラス内の属性 lst が顕になる
6Out[3]: [2, 3]
7In [4]: container[1:3]        # <- __getitem()__ でクラス自体がリストのように振る舞える
8Out[4]: [2, 3]

のような使い方ができるようになります.自分であらたなコンテナや繰り返しクラスを作るときはお世話になります.

特殊メソッドに関しては,ここらへんが参考になります

7.1.3. 継承に関して

さらに, さきほどのクラス Point2D を継承して,3次元上の点を表すための クラス Point3D を作ることとします. この新しいクラスには,さらにもう一つの座標系 z を加えるものとします.

 1import math
 2
 3class Point3D(Point2D): # Point2D を継承する場合の書き方
 4   def __init__(self, x=0.0, y=0.0, z=0.0):
 5      super().__init___(x, y)  # 継承元(superクラス,この場合はPoint2D)のコンストラクタを呼び出し
 6      self.z = z
 7
 8   def L2norm(self):
 9      # return math.sqrt((super.L2norm())**2 + self.z**2) でも可(読みにくい)
10      return math.sqrt(self.x**2 + self.y**2 + self.z**2)
11
12   def __add__(self, other):
13      return Point3D(self.x + other.x,
14                     self.y + other.y,
15                     self.z + other.z)

継承した Poitn3D のコンストラクタでは,継承元(スーパークラスと呼びます)である Point2D クラスのコンストラクタを引数付で呼び出して, self.xself.y を設定させます. その上で,足りない z 座標を設定しています.

さらに L2norm() において,3 次元の座標に拡張すべく, \(\sqrt{x^2 + y^2}\) から, \(\sqrt{x^2 + y^2 +z^2}\) で上書き(オーバーライド)しています.

また,特殊メソッドである __add__() も3次元に対応すべくオーバライドしています.

使い方は, Point2D とほとんど一緒(そうなるように設計すべき)で,

1In [1]: s = Point3D(1, 2, 3)
2In [2]: t = Point3D(4, 5, 6)
3In [3]: s.L2norm()
4Out[3]: 3.7416573867739413
5In [4]: u = s + t
6In [5]: u.z
7Out[5]: 9

といった使い方になります.

以上簡単な例で見てきましたが, クラスとオブジェクト指向プログラミングの恩恵を受けることで, 我々が遭遇する様々なオブジェクトに対応して 独自のメソッドや属性を持った, 様々なクラスにコードをまとめることができます. さらに継承を使うことで基底クラスを中心とした変種を考える, コードを 再利用 できます.

詳しくは http://www.python.jp/doc/release/tutorial/classes.html あたりを参照してください.