2. クラスとメソッド定義

オブジェクト指向プログラミング(object-oriented programming)とは、1960年代のソフトウェア危機 のあと、複雑なプログラム(つまり、ソフトウェア)を効率高く開発するために考案されたプログラミング手法です。

現在のソフトウェア開発におけるスタンダードなプログラミング手法になっています。オブジェクト指向を理解しなければ、プログラミングできないだけでなく、コードも読むことも難しくなります。

まずは、基本的な考え方を抑えておきましょう。

2.1. 基本な考え方

オブジェクト指向プログラミングを理解するための基本的な概念は次のようなものがあります。

  • 全てはオブジェクト(object)

  • メッセージング: プログラムは、オブジェクト間の メッセージ通信 (messaging) とみなす

  • カプセル化: クラス(class) でオブジェクトを設計し、外から見える必要がないものはみえなくする

  • クラス継承(抽象化): プログラムの共通部分に焦点をあて、クラスを設計する

2.2. データ構造

オブジェクト指向言語は、歴史的にはSimula3の抽象データ型(ADT)から発展してきたとされます。 同じく、データ型からオブジェクト指向を理解していきましょう。

Python は、今まで学んだきた通り、便利なデータ型を提供してくれます。

  • 論理値(bool): True, False

  • 整数(int): 123

  • 浮動少数点数(float): 1.23

  • 文字列(str): "123"

  • リスト(list): [1, 2, 3]

  • タプル(tuple): (1, 2, 3)

ところが、世の中のデータは、これらのデータ型だけでは十分ではありません。

2.2.1. xy 平面上の点

xy 平面上の点P(1,2)を表現する方法を考えてみましょう。

b70afa8161c24a5fb51a77171ed319dd

点Pの x座標, y座標はそれぞれ数値で表現できます。しかし、数値で点を表現すると、ひとつの点につき、px, py のように2つの変数が必要になります。

[1]:
px = 1
py = 2
qx = 2
qy = 3

print(f'P座標 ({px}, {py})')
print(f'Q座標 ({qx}, {qy})')


P座標 (1, 2)
Q座標 (2, 3)

もしプログラム内で扱う点の数が増えたら、点を扱うのが難しくなります。xy平面上の点として、x座標とy座標の値を、1つのデータ構造として、ひとまとまりで扱いたくありませんか?

データ構造

複数の値からなるデータをひとつの変数から扱えるようにすること

2.2.2. タプル

Python は、既に学んできたとおり、リストやタプルなどの複数の値をまとめて扱うデータ構造があります。

タプルを使ってみると:

[2]:
p = (1, 2)
q = (2, 3)

print(f'P座標 ({p[0]}, {p[1]})')
print(f'Q座標 ({q[0]}, {q[1]})')

P座標 (1, 2)
Q座標 (2, 3)

x座標とy座標をひとまとまりのデータとして扱えるようになります。 しかし、タプルを用いて、平面上の点を記述すると新たな問題も発生します。

課題点1: 直感的に、x,y 座標の値を理解しにくい。 次は、点Pと点Qの距離を計算する式ですが …

sqrt((p[0]-q[0])**2 + (p[1]-q[1])**2))

課題点2: 演算子の意味が異なる。 もし、点Pと点Qを加算してみたら、期待通りの結果が得られますか?

p + q

やはり、平面上の点を表現するための、より目的に特化したデータ構造があるといいと思いませんか?

オブジェクト指向

目的にあったデータ構造を定義するクラス機能を提供する

2.3. クラスとオブジェクト

クラス(class)は、データ構造を設計するプログラミング機能です。

まずは、先ほどの平面上の点 (x,y) を表す Point クラスを作ってみましょう。

xy平面上の点 Pointクラス

[4]:
class Point(object):
    x: int
    y: int
    def __init__(self, x, y):
        self.x = x
        self.y = y

Pointクラス上で定義された変数xyのことをプロパティ(Java: フィールド, C++: メンバ変数)と呼びます。平面上の点(x,y)に相当します。

__init__は、コンストラクタと呼ばれる特殊な関数です。新しいデータを生成するときに、初期化するために使われます。selfは、自己参照変数と呼ばれる特殊な変数で、データの自分自身を表します。

2.3.1. インスタンス化

クラスは設計図です。実体のあるデータではありません。オブジェクト指向言語では、クラスから作られたデータのことを、オブジェクト(object) 、もしくはインスタンス(instance) と呼びます。

961de3cb768f44018553be4e848b10ac

クラスのインスタンス化

[5]:
p = Point(1, 2)

Pointはクラス名ですが、関数のように呼び出すことができます。このとき、Pointクラス内で定義されたコンストラクタ__init__(self, x, y)が呼び出され、与えられた引数の値をプロパティに持つオブジェクトpが生成されます。

オブジェクトのプロパティは、p.xのように参照して、値をえます。

p.x (pのxプロパティ)

[6]:
print(p.x)

1

p.y (pのyプロパティ)

[7]:
print(p.y)

2

Let’s try

P(1,2)とQ(2,3)をインスタンス化して、そのx,y座標を表示してみよう

[8]:
p = Point(1, 2)
q = Point(2, 3)

print(f'P座標 ({p.x}, {p.y})')
print(f'Q座標 ({q.x}, {q.y})')

P座標 (1, 2)
Q座標 (2, 3)

2.3.2. インスタンスかどうか判定する

オブジェクト指向プログラミングを始めると、いろいろな種類のオブジェクトが登場します。あるオブジェクトが何のクラスか知りたいことも多くなります。

isinstance(x, c)は、x が クラス c のインスタンスであるか判定する関数です。

pは、Pointクラスのインスタンスかどうか?

[9]:
isinstance(p,Point)

[9]:
True

(1,2)は、Pointクラスのインスタンスかどうか?

[10]:
isinstance((1,2), Point)

[10]:
False

クラス名を調べる

Pythonは、すべてがオブジェクトです。整数値0も文字列も全て何かしらのクラスのオブジェクトになっています。xのクラス名は、type(x)関数で調べることができます。

整数値 1のクラス名を調べる

type(1)

2.3.3. 点P,Qの中間点を求める

Pointクラスに戻って、少し例題で練習しておきましょう。

例題(中間点を求める)

任意の2点P, Qの中間点を求める関数 mid(p, q)を定義してみよう。

p = Point(1, 1)
q = Point(3, 3)

m = mid(p, q)

print(f'中間点 ({m.x}, {m.y})')

ポイントは、関数 mid(p, q)の結果も、Point オブジェクトになる点です。

7bf3b9ef8d6f470091a93b9c5f404763

関数 mid(p,q) は、中間座標を計算したあと、新しく Point クラスをインスタンス化して、 Point オブジェクトとして結果を返します。

[11]:
def mid(p, q):
    mx = (p.x + q.x) // 2
    my = (p.y + q.y) // 2
    return Point(mx, my)

p = Point(1, 1)
q = Point(3, 3)
m = mid(p, q)

print(f'中間点 ({m.x}, {m.y})')


中間点 (2, 2)

Let’s try

任意の2点間の距離を求める関数distance(p,q)を定義してみよう。

[12]:
def distance(p, q):
    return # 自分で完成させよう

2.4. クラスとメソッド

メソッドは、クラス上に定義されたオブジェクトに対する操作を記述する関数です。メソッドを定義すると、オブジェクトがますます便利に使えるようになります。

メソッドを定義すると

mid(p,q) の代わりに、p.mid(q) のように書ける

メソッドの定義方法

  • クラス定義 (class) 内で定義する

  • 操作の対象となるオブジェクトは自分自身 (self) になる

まずは、先ほどの mid(p,q) 関数をメソッドとして定義してみます。

関数定義のメソッド化

  • 関数定義をクラス内のブロックに移動させる

  • 全体的をインデントを下げる

  • 関数の第一引数 を self に変更する

[13]:
class Point(object):
    x: int # 省略してもよい
    y: int # 省略してもよい

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def mid(self, q): # 先 ほ ど のmid(p,q)のメソッド化
        mx = (self.x + q.x) // 2
        my = (self.y + q.y) // 2
        return Point(mx, my)

プロパティの値は、p.xのようにオブジェクトの変数pから参照します。しかし、メソッド定義からみると、自分自身に何の変数名が与えられているかわかりません。そこで、自己参照変数 selfを用います。

Let’s try

点P(1,1),Q(3,3)の中間点をメソッドを用いて、求めてみよう。

[14]:
p = Point(1, 1)
q = Point(3, 3)
m = p.mid(q)

print(f'中間点 ({m.x}, {m.y})')

中間点 (2, 2)

クラス定義を始めると、自己参照変数(self) の意味が最も理解しにくいところです。多くの学生が何度か書いたら、「突然わかる」ようになるといいます。 Colabは、Pythonを試してみるのに優れた環境なので、何度かメソッドを書いてみてください。

Let’s try

任意の2点間の距離を求める関数distance(p,q)もメソッドとして定義してみよう。

[ ]:
    def distance(self, q):
        return # 自分で完成させよう

2.4.1. オブジェクトの表示 __repr__

Python では、__repr__(self) というメソッド名は、オブジェクトの内部を文字列として表示のための特別なメソッド名になっています。

__repr__(self)の定義前: オブジェクトの内部は見えない

[15]:
Point(1, 2)

[15]:
<__main__.Point at 0x110ca6c10>

__repr__(self)を定義する

[21]:
class Point(object):
    x: int
    y: int
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def distance(self, q):
        dx = self.x - q.x
        dy = self.y - q.y
        return math.sqrt(dx**2 + dy**2)

    def __repr__(self):
        return f'({self.x}, {self.y})'

__repr__(self)の定義後

[17]:
Point(1, 2)

[17]:
(1, 2)

クラスを定義したら

__repr__(self) も定義しておくと、オブジェクトの内容が見えるので、開発がしやすくなります。

2.5. 空間上の点

ここまでの練習として、空間上の点を表現するクラスを定義してみましょう。

例題(空間上の点)

空間上の点 (x, y, z) を表現するクラスPoint3Dを定義せよ。また、空間上の2点間の中間点 (mid) と距離 (distance) を求めるメソッドも定義せよ。

p = Point3D(1.0, 2.0, 3,0)
q = Point3D(4.0, 4.0, 4.0)

print('点P', p)
print('点q', q)
print('中間点', p.mid(q))
print('距離', p.distance(q))

04cdb3f814f74a07a84de6f3284de245

[22]:
import math

class Point3D(object):
    x: float
    y: float
    z: float
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

    def __repr__(self):
        return f'({self.x},{self.y},{self.z})'

    def mid(self, q):
        x = (self.x + q.x) / 2
        y = (self.y + q.y) / 2
        z = (self.z + q.z) / 2
        return Point3D(x, y, z)

    def distance(self, q):
        dx = self.x - q.x
        dy = self.y - q.y
        dz = self.z - q.z
        return math.sqrt(dx**2 + dy**2 + dz**2)

2.5.1. 動的束縛

クラス Point と Point3D を比較したとき、注目したいところはメソッド名です。

クラスが違えば、同じメソッド名を定義することができます。しかもクラスごとに異なる処理を行うことができます。

さらに、注目して欲しいのは、2次元版も3次元版も全く同じコード p.distance(q) で距離を計算できています。

Pointの場合

[23]:
p = Point(1,2)
q = Point(2,3)
print(p.distance(q))

1.4142135623730951

Point3Dの場合

[25]:
p = Point3D(1,2,3)
q = Point3D(2,3,4)
print(p.distance(q))

1.7320508075688772

オブジェクト指向プログラミングの大きな特徴に動的束縛 (dynamic binding) という機構がありま す。これは、実行時に、クラスの種類によって呼び出すメソッドを決定してくれるものです。つまり、pがPointクラスなら、Pointクラスのメソッド、pがPoint3Dクラスなら、Point3Dクラスのメソッドが呼び出されます。

動的束縛

動的束縛は、if 文などの条件分岐と等価な役割を果たしています。もし、動的束縛を使わないと、次のようにpのクラスごとに処理を切り替えるように書くことになります。

[ ]:
import math

def distance(p, q):
    if isinstance(p, Point):
        dx = p.x - q.x
        dy = p.y - q.y
        return math.sqrt(dx ** 2 + dy ** 2)

    if isinstance(p, Point3):
        dx = p.x - q.x
        dy = p.y - q.y
        dz = p.z - q.z
        return math.sqrt(dx ** 2 + dy ** 2 + dz ** 2)

動的束縛を活かしたプログラミングは、次回、ポリモーフィズム としてさらに詳しく取り上げます。

2.6. コースワーク

有理数とは、2つの整数 \(a\), \(b\) (ただし \(b\)\(0\) でない)を用いて、

\[\frac{a}{b}\]

という分数で表せる数です。 \(b = 1\) とすることにより、任意の整数は有理数となります。

演習課題(有理数)

有理数をクラスで定義してみよう。

  1. 有理数クラスを定義する (クラス名は、Q、プロパティはabを用いるとよい。)

  2. クラスQをインスタンス化し表示できるようにする。例えば、Q(1,2)は、1/2と表示される. Q(2,1)は、2と表示される。

  3. 約分も忘れずに: Q(2,4)は、1/2と表示される。

  4. 整数も表せるようにする. 任意の整数x に対し、Q(x,1)xと表示される。

  5. 加算(add)できるようにする。つまり、Q(1, 2).add(Q(1, 3))は、5/6になる

  6. 四則演算を全部、実装する

d4d393db51a04b92b65d25ec7168d8a0

  1. (上級者向け)次週の演算子オーバーロードを予習し、Q(1,2) + Q(1,3)のように加算できるようにする。

2.7. (補足) 演算子オーバーロードについて

演算子オーバーロード (operator overloading) とは、特殊なメソッドを定義することで、演算子を拡張するオブジェクト指向プログラミングの技法です。

2.7.1. Pythonにおける演算子

まず、Python において、そもそも演算子がどのように実装されているか、確認するところから始めましょう。

Python は、純粋なオブジェクト指向プログラミング言語なので、全ての値はオブジェクトになっています。1という整数値も、実はintクラスのオブジェクトです。

[ ]:
type(1)

オブジェクトへの操作は、全てメソッドを通して行われます。加算は、__add__(x)という特殊なメソッドを呼び出した結果です。

[ ]:
(1).__add__(2)

毎回、(1).__ add__(2)のように書くのはつらいので、メソッドコールのシンタックスシュガー として、1+2が用意されているわけです。

417d42d4f1da48e38b3a18f791f424f0

シンタックスシュガー(糖衣構文)

機械的に置き換えられる簡易記法

とういことで、__add__(self, x)を定義すれば、加算演算子を定義することができます。

四則演算と演算子オーバーロード

  • x.__add__(y) : x + y

  • x.__sub__(y) : x - y

  • x.__mul__(y) : x * y

  • x.__truediv__(y): x / y

  • x.__floordiv__(y): x // y

Pythonでは、上記以外にもほぼ全ての演算子がオーバーロードすることができます。