第11回 クラス、オブジェクト#


Open In Colab


この授業で学ぶこと#

これまで組み込み型のデータを利用してプログラムを書いてきたが、ここでは新しいデータ型を作成する方法を学ぶ。

クラス#

第4回で学んだメソッドやオブジェクト、クラス、インスタンスといった概念について、簡単に復習しておこう。 まず、データ型に専属の関数のことをメソッドと言う。 そして、データとメソッドを合わせてオブジェクトと言い、オブジェクトの種類をクラス、クラスに基づいて作られる具体的なオブジェクトのことをインスタンスと言うのであった。

新しいデータ型は、クラスを定義することで作ることができる。クラスは次の図のような形で定義する。

_images/class.png

Fig. 26 classの書き方#

if文やfor文のように、ブロックのコードはインデントを入れてから書き始める。あとで見るようにブロックにはメソッドを定義していく。

まずは最も簡単なクラスを定義してみよう。

class Apple:
    pass

クラス名は自由に設定することができるが、1文字目のみを大文字にするという慣例がある。一方でメソッド名や関数名は全て小文字で書くのが慣例となっている。

このクラスのインスタンスは次のようにして作成する。

a = Apple()

このインスタンスにデータを持たせよう。例えば、栄養を表す nutrition というデータを次のように付与することができる。 このデータのことを属性アトリビュート)という。

a.nutrition = 10
print(a.nutrition)
10

この属性は a というインスタンスに固有のものである。クラスが同じであっても、各インスタンスの持つ属性は別々に管理される。

b = Apple()
b.nutrition # こちらのインスタンスには属性は定義されていない
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
/var/folders/p0/0zrw9jsj03v3vlszs8v3m2p40000gn/T/ipykernel_9205/1585116261.py in <module>
      1 b = Apple()
----> 2 b.nutrition # こちらのインスタンスには属性は定義されていない

AttributeError: 'Apple' object has no attribute 'nutrition'

初期化メソッド#

インスタンス1つ1つに属性を設定していくのは大変なので、インスタンスの生成時に最初から属性を持たせたくなる。 これを実現するのが初期化メソッドである。 初期化メソッドは、classのブロック中に __init__() という名前の関数として定義する。 ただし普通の関数とは異なり、第一引数に必ず self という変数を設定する。 これはインスタンス自身を渡すための変数である。 そして初期化メソッド内では、self.属性名 = という代入文で、属性名とその値を設定する。

class Apple:
    def __init__(self):
        self.nutrition = 10
        
a = Apple()
print(a.nutrition)  # 初期化の時点で10という値が設定されている。
10

初期化メソッドに引数を追加することで、nutrition の値を外から設定できるようにすることもできる。 このとき __init__() に設定した第二引数以降が、クラス名() と呼び出すときの引数に対応する。

class Apple:
    def __init__(self, nutrition):
        self.nutrition = nutrition
        
a = Apple(15)
print(a.nutrition)
15

初期化メソッドのようにメソッド名の両端にアンダースコア2つ __ をつけたメソッドのことを 特殊メソッド という。 特殊メソッドは次に説明する普通のメソッドとは異なり、その用途に応じてメソッド名が決められている。 また普通のメソッドのように データ.メソッド名() という呼び出し方をしない。

メソッド#

普通のメソッドの定義の仕方は、初期化メソッドと同様である。 ここでは日にちを1日経過させるメソッドとして step() を定義してみよう。 まずは初期化メソッドの中で、日にちを管理する属性として day を定義しておく。 次にclassブロック中に step() という関数を追加する。 ここでもやはり、第一引数に self を設定することに注意する。

step() 関数の中で self.属性名 と書くことで、そのメソッドを呼び出したインスタンスの属性値にアクセスすることができる。 step() では日にちを1日経過させ、それに合わせて栄養価を変化させてみよう。

class Apple:
    def __init__(self, nutrition):
        self.day = 0
        self.nutrition = nutrition
        
    def step(self):
        self.day += 1
        if self.day < 14:
            self.nutrition += 1
        else:
            self.nutrition -= 1
a = Apple(15)
print(a.nutrition)

a.step() # 1日経過
print(a.nutrition)
15
16

練習1
Apple クラスを真似して Banana クラスを作成しなさい。Bananastep() の内容は、経過日数が7日未満のとき栄養価が1上がり、7日以上のとき栄養価が1下がるとすること。

class Banana:
    pass
# 完成したら以下でテストする

a = Banana(10)
print(a.nutrition) # 10

a.step()
print(a.nutrition) # 11

for i in range(7):
    a.step()
    
print(a.nutrition) # 14

解答例

class Banana:
    def __init__(self, nutrition):
        self.day = 0
        self.nutrition = nutrition
        
    def step(self):
        self.day += 1
        if self.day < 7:
            self.nutrition += 1
        else:
            self.nutrition -= 1

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

作成した Apple()Banana() を用いて、栄養価のシミュレーションを行ってみよう。 手元にりんごが2つ、バナナが2つあり、それぞれの栄養価の初期値は10であったとする。 このとき例えば経過日数ごとの栄養価の合計は次のように求められる。

basket = [Apple(10), Apple(10), Banana(10), Banana(10)]

for day in range(1, 21):
    total = 0
    for fruit in basket:
        fruit.step()
        total += fruit.nutrition
        
    print(f"{day}日目: {total}")
1日目: 44
2日目: 48
3日目: 52
4日目: 56
5日目: 60
6日目: 64
7日目: 64
8日目: 64
9日目: 64
10日目: 64
11日目: 64
12日目: 64
13日目: 64
14日目: 60
15日目: 56
16日目: 52
17日目: 48
18日目: 44
19日目: 40
20日目: 36

このプログラムは、処理としてはそれなりに複雑なことをしているが、コードとしてはとてもシンプルである。 特に一度 Apple()Banana() を定義したあとでは、経過日数ごとの栄養価の変化のルールを忘れてしまい、単に step() メソッドを呼び出すだけでよくなっている。 これと同じようなプログラムをクラスを使わずに実現しようとすると、day の値と fruit の種類ごとに栄養価を変化させる処理をfor文の中で書くことになり、コードが複雑化する。

このようにクラスを利用して、データとそれに付随する操作をオブジェクトにまとめて隠してしまうことで、コードの見通しと再利用性を向上させることができる。オブジェクトを利用したプログラミングスタイルのことをオブジェクト指向と言うが、これがオブジェクト指向の利点の1つである。 オブジェクト指向は、規模の大きいプログラムを書く際に重宝する。

演習#

第4回の授業で、浮動小数点数は小数を完璧な精度で表現しないという話をした。 ここでは分数を表すクラスを作成し、それにより 0.1 * 3 のような計算を正確に行えるようにしよう。

雛形のクラスを以下に用意した。 クラス名は Fraction とし、初期化メソッドで分子(numeratorを略して n)と分母(denominatorを略して d)を設定できるようにしている。

class Fraction:
    def __init__(self, n, d):
        self.n = n
        self.d = d
        
    def __str__(self):
        return f'{self.n}/{self.d}'
    
    def add(self, other):
        n = self.n * other.d + other.n * self.d
        d = self.d * other.d
        return Fraction(n, d)

__str__()print() 関数に渡されたときの表示を定める特殊メソッドである。 上のように定義すると、以下のように動作する。

print(Fraction(1, 3))

add() は分数の足し算を行うメソッドである。Apple クラスにおける step() メソッドとは異なり、add() メソッドは戻り値を持ち、計算結果の Fraction インスタンスを返していることに気をつけよう。

例えば以下のように動作する。

x = Fraction(1, 3)
y = Fraction(1, 5)
print(x.add(y))

課題1
足し算の定義を真似して、掛け算を行うメソッド mul() を定義しなさい。

課題2
Fraction クラスのインスタンスが必ず既約分数(分母と分子に1以外の公約数がなくて、それ以上に約分できない分数のこと)を表すように __init__() の処理を変更しなさい。そのためには引数に渡される nd を最大公約数で割ったものを、self.nself.d に設定すればよい。self.nself.dint 型としたいので、割り算の結果が int 型となるように割り算には / ではなく // を使うこと。また最大公約数は math モジュールの math.gcd() 関数で求めることができる。

import math

print(math.gcd(12, 8))  # math.gcdの使い方(12と8の最大公約数を求めている)
print(math.gcd(4, 3))  # math.gcdの使い方(4と3の最大公約数を求めている)
class Fraction:
    def __init__(self, n, d): # 課題2: 以下を書き換える
        self.n = n
        self.d = d
        
    def __str__(self):
        return f'{self.n}/{self.d}'
    
    def add(self, other):
        n = self.n * other.d + other.n * self.d
        d = self.d * other.d
        return Fraction(n, d)
    
    def mul(self, other):
        pass # 課題1: ここに適切なコードを書く

コードが完成したら以下を実行してみよう。正しく実装できていれば、3/102/5 と表示されるはずである。

x =  Fraction(1, 10)
y = Fraction(3, 1)
print(x.mul(y))

x = Fraction(2, 3)
y = Fraction(3, 5)
print(x.mul(y))

おまけ
特殊メソッドの __add__()__mul__() を使うと、それぞれ + 演算子、* 演算子を使用したときの動作を定義することができる。 上で作成した Fraction クラスの add()mul() の名前を __add__()__mul__() に変更して、クラス定義のコードを実行した上で、次のコードを実行してみよう。

print(Fraction(1, 3) + Fraction(1, 5))
print(Fraction(2, 3) * Fraction(3, 5))