第10回 クラス#


Open In Colab


この授業で学ぶこと#

前回は新しい関数を定義する方法について学んだ。今回はさらに進んで、新しいデータ型を定義するための方法であるクラスについて学ぶ。クラスを利用することで、データとそれを操作するメソッドを一つのまとまりとして扱えるため、関数と同様にコードの再利用性を高め、より直感的で理解しやすいコードを書くことができるようになる。

クラス#

第4回で学んだメソッド、オブジェクト、クラス、インスタンスの概念を簡単に復習しておこう。まず、特定のデータ型に属する関数をメソッドという。次に、データとメソッドを一緒にまとめたものをオブジェクトと呼ぶ。そして、そのオブジェクトの設計図となるものがクラスであり、クラスをもとに実際に作成されたオブジェクトをインスタンスという。なお、データ型とはオブジェクトの種類を示す言葉であり、クラスはそのデータ型を定義する設計図であるとも言える。

新しいデータ型(オブジェクト)は、クラスを定義することで作ることができる。クラスは次の図のように定義する。

_images/class.png

Fig. 19 classの書き方#

クラス定義はヘッダーとブロックから構成され、ヘッダーには class に続いてクラス名を書く。そして、ブロックにメソッドを定義していく。メソッドの定義の書き方は普通の関数とほぼ同じであるが、第一引数は必ず self にするという決まりがある。これはインスタンス自身を渡すための変数である。

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

class Apple:
    pass 

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

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

a = Apple()

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

a.nutrition = 10
print(a.nutrition)

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

b = Apple()
b.nutrition # こちらのインスタンスには属性は定義されていない

初期化メソッド#

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

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

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

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

初期化メソッドのようにメソッド名の両端にアンダースコア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)

練習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

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

作成した AppleBanana を用いて、栄養価のシミュレーションを行ってみよう。 手元にりんごが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}")

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

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

演習#

第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() 関数に渡されたときの表示を定める特殊メソッドである。 上のように定義すると、以下のように動作する。

x = Fraction(1, 3)
print(x)

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

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

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

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

練習3
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): # 練習3: 以下を書き換える
        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 # 練習2: ここに適切なコードを書く

コードが完成したら以下を実行してみよう。正しく実装できていれば 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 クラスの addmul の名前を __add____mul__ に変更して、クラス定義のコードを実行した上で、次のコードを実行してみよう。

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