レッスン118
クラス入門⌗
Pythonには「クラス(class)」と呼ばれる仕組みがあります。 この「クラス」は、どのような仕組みで、どのように使用するのか簡単に説明します。
オブジェクト指向プログラミング⌗
「クラス」は「オブジェクト指向プログラミング(Object Oriented Programming, OOP)」といわれるプログラミング手法で使用される仕組みです。 では、この「オブジェクト指向プログラミング」とはどんな手法でしょう。
「オブジェクト指向プログラミング」とは「モノ(オブジェクト)」とその「モノ」が「どんなもので、どう動くか」に注目する考え方です。
例えば、あなたがシューティングゲーム(射撃ゲーム)を作ろうとしているとしましょう。
このシューティングゲームは以下のようなシナリオだとします。
- あなたは、HPが3で3回相手の弾に当たると死んでゲームが終了し、あなたの負けになる
- あなたは、上下に移動することができる
- あなたは、好きなタイミングで弾を撃つことができる
- 敵のモンスターは、最初はHPが1であなたの弾に1回あたると死んでしまう
- 敵のモンスターが死ぬと、HPが1つ多い新たなモンスターが現れる
- 最後にHPが5のラスボスモンスターが現れ、このモンスターを倒すとゲームが終了し、あなたの勝ちになる
- 敵のモンスターは上下にランダムに移動する
- 敵のモンスターはランダムに弾を打ってくる
このゲームの基本的な登場人物は以下の通りです。
- 自分(あなた)
- 敵(モンスター)
- 弾(あなたと敵の弾)
「オブジェクト指向プログラミング」ではこの登場人物(モノ、オブジェクト)に注目し、「どんなモノ(属性)」で「どう動くか(メソッド)」を考えます。
まず、自分に注目してみます。
- 自分はHPが3(どんなモノ)
- 自分は上下に動ける(どう動くか)
- 自分は弾を撃てる(どう動くか)
次に、敵のモンスターに注目します。
- 敵のHPは1〜5(どんなモノ)
- 敵は上下にランダムに動く(どう動くか)
- 敵はランダムに弾を撃つ(どう動くか)
最後に弾に注目します
- 弾はあなたの弾と敵の弾がある(どんなモノ)
- 一定の速度で移動する(どう動くか)
これらを登場人物ごとに整理してみましょう
| 番号 | 1 | 2 | 3 |
|---|---|---|---|
| 登場人物 | 自分 | 敵 | 弾 |
| どんなモノ(属性) | HPが3 | HPが1〜5 | 自分の弾か敵の弾 |
| どう動く(メソッド) | 上下に動く | 上下にランダムに動く | 一定の速度で移動する |
| どう動く(メソッド) | 弾を撃つ | 弾をランダムに撃つ |
ゲームは、この登場人物を組み合わせてプログラムを作っていけば出来上がります。
例えば
- 「1 自分」の「3 弾」が「2 敵」に当たったら「2 敵」のHPが1つ減る
のようなプログラムを先ほどのシナリオに合わせて作っていけば良いことになります。
このように「自分」「敵」「弾」といった「モノ(オブジェクト)」に注目して、プログラムを作っていくことを「オブジェクト指向プログラミング」と言います。
クラスとインスタンス⌗
クラスとは、先ほど整理した「モノ」ごとに「どんなモノ(属性)」「どう動く(メソッド)」を定義した設計図のことです。
「どんなモノ」のことを「クラスの属性」、「どう動く」のことを「クラスのメソッド」と呼びます。
そして、設計図に基づいて作られた「自分」「敵」「弾」などの実際のモノを「インスタンス」と呼びます。
と言われても、さっぱりわかりませんよね。
例えば、敵が死ぬと新たな敵が現れます。このとき新たに現れる敵はあらかじめ作られた設計図(クラス)から召喚(作成)されて、インスタンス(敵のモンスターの実体)になります。 弾は、自分や敵から撃たれると、召喚(作成)されてインスタンス(弾の実体)になります。 なお、自分や最初の敵は、プログラムの開始時に1回だけ召喚(作成)されインスタンス(実体)になっています。
実際のpythonのプログラムで見てみましょう。
class Player: # 自分です
def __init__(self): # 自分が召喚(作成)されたときに呼ばれます。
self.hp = 3 # HPは3です。
self.y = 0 # 最初は上下の中央にいます
self.x = -100 # 自分は左端にいます
# self.hp, self.y, self.x がクラスPlayerの属性です。
def move(self, dy): # 上下(+は上、-は下)に動きます(クラスのメソッド)
self.y += dy
def shot(self): # 弾を打ちます(クラスのメソッド)
return Shot(True, self.x, self.y) # 自分の弾を自分の位置で召喚(作成)して返します。
class Monster: # 敵のモンスターです。
def __init__(self, hp): # 召喚(作成)されたときに、HPを指定して呼ばれます。
self.hp = hp # 召喚(作成)時に指定したHPを設定します
self.y = 0 # 最初は上下の中央にいます
self.x = 100 # 自分は右端にいます
# self.hp, self.y, self.x がクラスMonsterの属性です。
def move(self): # ランダムに上下します。(クラスのメソッド)
self.y += random.randint(-50, 50) # ±50でランダムに移動します
def shot(self): # ランダムに弾を打ちます。(クラスのメソッド)
if random.randint(0, 9) == 0: # ランダムに10回に1回
return Shot(False, self.x, self.y) # 敵の弾を敵の位置で召喚(作成)して返します。
return None # 撃たないときは空(None)を返します
class Shot: # 弾です。
def __init__(self, isPlayer, x, y): # 召喚(作成)されたとき、自分か敵かと位置を指定して呼ばれます。
self.isPlayer = isPlayrt # 自分か敵かを設定します
self.x = x # 位置を設定します
self.y = y
# ここまではクラス定義(設計図)ですぐには実行されません。
player = Player() # ゲーム開始時に上記で定義された自分の設計図(クラス)に基づいて自分を召喚(作成)します。
monster = Monster(1) # ゲーム開始時に上記で定義された最初の敵の設計図(クラス)に基づいて、HP1のモンスターを召喚(作成)します。
class Player以下が設計図になり、player = Player()で設計図(クラス)からインスタンス(プレイヤーの実体)が作成され、player変数に設定されます。
同様に、class Monster以下が設計図になり、monster = Monster(1)で設計図(クラス)からインスタンス(モンスターの実体)が作成され、monster変数に設定されます。
クラスの利点⌗
クラスを使うとどんないいことがあるでしょう。 代表的なものは以下の4つです。
- コピペしなくても同じ実体(インスタンス)をいっぱい作れる
- モノ(オブジェクト)ごとに整理されていて、修正したり、追加したりしやすい
- 元のクラスを変更せずに、属性やメソッドを追加した新たなクラスを作ることができる
- モノ(オブジェクト)が違っていても、同じプログラムを使える仕組み(ポリモーフィズム)がある
これらを1つずつ説明します。
コピペしなくても同じ実体(インスタンス)をいっぱい作れる⌗
クラスを使うことで、同じ設計図でたくさんの実体を作ることができます。
例えば、さきほどのシューティングゲームの例では、必要な時に敵を召喚(Monster()を呼ぶ)ことで、いくらでも敵の数を増やせます。 クラスを使えば
monster1 = Monster(1)
monster2 = Monster(2)
monster3 = Monster(3)
のように簡単に3種類のモンスターが作れます。 もしクラスを使わなければ、
monster_1_hp = 1
monster_1_y = 0
monster_1_x = 100
monster_2_hp = 2
monster_2_y = 0
monster_2_x = 100
monster_3_hp = 2
monster_3_y = 0
monster_3_x = 100
のようにモンスターごとに同じような属性や関数を用意しなければいけなくなり、コピペのかたまりになってしまいます。
モノ(オブジェクト)ごとに整理されていて、修正したり、追加したりしやすい⌗
モノ(オブジェクト)に関係するもの(属性やメソッド)がクラスとしてまとまっているので、モノ(オブジェクト)に関係するものを修正したり、追加する時、あちこちのプログラムを修正することなく、モノのクラスだけ修正すれば済むようになります。
また、カプセル化といって、そのクラス以外のプログラムから変更させたくない属性や、実行させたくないメソッドをそのクラスの中だけでしか利用できないようにすることができる仕組みがあります。 例えばpythonでは、属性やメソッドの名前の頭に「_」を付けることで実現することができます。
class Student: # 生徒のクラス
def __init__(self, name, age): # 名前と年齢を指定してインスタンスを作成する
self._name = name # 名前の属性_nameは直接クラスの外からアクセスできない
self._age = age # 年齢の属性_ageは直接クラスの外からアクセスできない
def getName(self): # 名前を返すメソッド(クラスの外からアクセスできる)、クラスの名前を外から変更するメソッドはない
return self._name
def addAge(self): # 年齢を1つ加算するメソッド(クラスの外からアクセスできる)、ただしクラスの外から任意に変更するメソッドはない
self._age += 1
def getAge(self): # 年齢を返すメソッド(クラスの外からアクセスできる)
return self._age
例えば、このような生徒クラスを作ることで、生徒クラスの外からは名前と年齢を参照できるが、最初に設定した名前は後から変更できず、年齢の加算だけできるというように、生徒の情報を管理するができるようになります。
また、生徒に関する属性やメソッドは、このクラス内で完結しているので、修正・追加もこのクラスだけで行えます。
元のクラスを変更せずに、属性やメソッドを追加した新たなクラスを作ることができる⌗
例えば、先ほどのシューティングゲームで敵のモンスターのラスボスだけは、「魔法を使う(相手を一定時間動けなくする)」ことができることにします。 ただし、それ以外の能力は通常のモンスターと一緒です。
こういう場合、モンスタークラスに「魔法を使う」というメソッドを追加してしまうと、ラスボス以外のモンスターも魔法を使えるようになってしまいます。 だからといって、モンスタークラスをコピペしてラストモンスタークラスを作ると、同じようなプログラムが増えていきます。 さらに、後からモンスタークラスを修正した場合、ラストモンスタークラスにも同じ修正をしなくてはならなくなり、面倒で修正ミスなどトラブルの元になります。
クラスでは「継承」という仕組みを使って元のクラスを修正せずに、属性やメソッドを追加した新たなクラスを作る仕組みがあります。
class Monster:
....
class LastMonster(Monster):
def magic(self):
.....
Pythonではこのように書くと、LastMonsterはMonsterクラスの属性やメソッドをそのまま引き継ぎ(継承し)、拡張したい部分(mogicメソッド)だけ、追加することが可能になります。 Monsterクラスは全く変更がありませんし、Monsterクラスの変更は、全て自動的にLastMonsterクラスにも反映されることになります。
また、この仕組みを積極的に使って、複数のクラスの共通部分をまとめたクラス(スーパークラス)を作り、そのクラスを継承させることで、プログラムをまとめることができます。 例えば、先ほどのシューティングゲームでは、自分もモンスターもHPと位置(x, y)を持っている部分は共通なので、例えばCharacterというスーパークラスを作り以下のようにまとめることができます。
class Character:
def __init__(self, hp, x, y):
self.hp = hp
self.x = x
self.y = y
class Player(Character):
def __init__(self):
super().__init__(3, 100, 0)
...
class Monster(Character):
def __init__(self, hp):
super().__init__(hp, -100, 0)
...
このようにすると、プレイヤーとモンスターで共通のメソッドを追加したい場合、Characterクラスにそのメソッドを作成すれば、わざわざ同じメソッドをプレイヤーとモンスターの各クラスにコピペして作りこむ必要はなくなり、修正する際も1つのメソッドの修正ですみます。
一方、プレイヤーとモンスターでどちらか単独に属性やメソッドを追加したい場合は、該当するクラスにだけ追加すればよいことになります。 さらに、メソッドが共通でも行う内容が異なる場合なども後述する「ポリフォリズム」の仕組みを使えば実現できます。
なお、このような共通クラスは通常、共通クラスだけのインスタンスを作ることはないので「抽象クラス」と呼ばれます。 PythonにはABCというライブラリを使って、「抽象クラス」を定義し、「抽象クラス」からインスタンスを作るとエラーになるという仕組みがあるので、これを使うことも可能です。
モノ(オブジェクト)が違っていても、同じプログラムを使える仕組み(ポリモーフィズム)がある⌗
クラスには「ポリモーフィズム」と呼ばれる「モノ(オブジェクト)が違っていても、同じプログラムを使える仕組み」があります。
例えば、先ほどのシューティングゲームで、ゲームのシステムは一定周期(例えば1秒に1回とか)で各登場人物を表示し直す仕組みになっているとします。 この場合、いろいろな登場人物に対して、この人物はこう描画するというようにプログラミングしていたら、登場人物が増えるたびにプログラムを書き換えなければならなくなってしまいます。
そこでItemという登場人物を共通化した抽象クラス(スーパークラス)を作成し、このクラスにdrawという描画を行うメソッドを定義して、描画したい場合、システムはどの登場人物に対しても、drawというメソッドを呼べばよいようにします。
from abc import ABC, abstractmethod
class Item(ABC): # インスタンスを作らない抽象クラス
@abstractmethod
def draw(self):
pass # 実際に呼ばれない仮想メソッド
class Character(Item, ABC): # このクラスもインスタンスを作らない抽象クラス
...
Class Player(Character): # 実際にプレイヤーのインスタンスを作るクラス
...
def draw(self):
# ここに自分を描画するプログラムを書く
print("draw player")
Class Monster(Character): # 実際にモンスターのインスタンスを作るクラス
...
def draw(self):
# ここにモンスターを描画するプログラムを書く
print("draw monster")
Class Shot(Item):
...
def draw(self):
# ここに弾を描画するプログラムを書く
print("draw shot")
items = [] # 描画する登場人物のリスト
player = Player()
items.append(player) # 登場人物を召喚したら登場人物リストに追加する
monster = Monster(1)
items.append(monster) # 登場人物を召喚したら登場人物リストに追加する
...
def draw(): # システムが一定周期で描画する際に呼ばれる関数
for item in items: # 登場人物を1人ずつリストから取り出し
item.draw() # 登場人物を意識せずに描画する(各登場人物に対応したクラスのdrawメソッドが呼ばれる)
システムは単純に各登場人物のdrawメソッドを呼びます。 すると登場人物に対応したクラスに実装されたdrawメソッドが呼ばれ、登場人物ごとに必要な描画の処理が行われます。
このように、描画の違いは各クラスごとのメソッドが対応してくれるので、呼ぶ方のシステムは同じやり方で呼ぶことができます。
今回は1つのメソッドなので単純ですが、Aというメソッドを呼び、Bというメソッドを繰り返し呼び、最後にCというメソッドを呼ぶというような複雑なプログラムでも、クラスごとにA,B,Cそれぞれのメソッドを用意することで、各クラスに対して同じプログラムで呼ぶことができるようになります。
なお、抽象クラスに実装されたdrawメソッドは、実際には各クラスに実装されたdrawメソッドの方が呼ばれるので、「抽象メソッド」と呼ばれ通常は実行されません。
ただし、pythonでは、各クラスにdrawメソッドが実装されていなければ、エラーにはならず、抽象メソッドの方が実行されてしまいます。(抽象メソッドの中身はpassなので実際は何も行われなくなってしまう)
このようなことを防ぐため、ABCというライブラリを使用し@abstractmethodという印を「抽象メソッド」の前に付けることで、「抽象メソッド」に対応するメソッドが各クラスで実装されていないとエラーとする仕組みがあります。
まとめ⌗
「クラス」とか「オブジェクト指向」というと非常に難しく聞こえますが、実際はプログラムを作る時、共通化できるものはできるだけ共通化して、できるだけ簡単に、バグがなく、メンテナンスのしやすいプログラムを作成するための方法です。
「クラス」を活用し、きれいで、効率的で、わかりやすいプログラムが書けるようになりましょう。