第8回:継承¶
前回までで、クラスにデータ(インスタンス変数)と動き(メソッド)を持たせられるようになった。今回は、すでにあるクラスを 元にして 新しいクラスを作る仕組み、継承(inheritance) を学ぶ。ゲームのキャラなら、まず「キャラの共通部分」を1つ作って、その上に「勇者ならではの動き」「魔法使いならではの動き」を 足していく イメージ。
なぜ継承が必要か¶
ゲームで Hero(勇者)と Wizard(魔法使い)の2クラスを作りたいとする。素直に書くと、両方に name hp take_damage show_status をコピペすることになる。
class Hero:
def __init__(self, name, hp):
self.name = name
self.hp = hp
def take_damage(self, damage):
self.hp = self.hp - damage
if self.hp < 0:
self.hp = 0
def show_status(self):
print(self.name, 'のHP:', self.hp)
def special(self):
print(self.name, 'の必殺技!')
class Wizard:
def __init__(self, name, hp, mp):
self.name = name
self.hp = hp
self.mp = mp
def take_damage(self, damage): # ← Hero と完全に同じ
self.hp = self.hp - damage
if self.hp < 0:
self.hp = 0
def show_status(self): # ← Hero とほぼ同じ
print(self.name, 'のHP:', self.hp)
def cast(self):
print(self.name, 'は呪文を唱えた!')
問題は2つ。
- 書く量が多い:共通部分を毎回コピペする。
- 修正漏れバグ:
take_damageの仕様(例えば「最低 HP は 1」に変えたい)を Hero だけ直して Wizard を直し忘れる、ということが起きる。
共通を1か所に書きたい
「共通部分」と「違う部分」を分けて、共通部分は1か所だけ書きたい。これを実現するのが 継承。
継承とは¶
- 親クラス(スーパークラス、基底クラス):共通部分をまとめたクラス。
- 子クラス(サブクラス、派生クラス):親クラスを引き継いだうえで、違いを足したクラス。
子クラスは親クラスの すべてのインスタンス変数とメソッドを自動的に受け継ぐ。子クラス側では「親と違う部分」だけを書けばよい。
classDiagram
Character <|-- Hero
Character <|-- Wizard
class Character {
name
hp
take_damage()
show_status()
}
class Hero {
special()
}
class Wizard {
mp
cast()
}
Python と継承¶
Python では、実はすべてのクラスが何かを継承している。何も指定しなければ自動的に object クラスを継承する。str も list も int も、たどっていけばすべて object の子クラス。
ふだん意識しないが、int が + で足し算できたり str が .upper() を持てるのは、こうしたクラスの仕組みのおかげ。
なお Python は 多重継承(親を複数指定する)も書ける。class C(A, B): のようにカンマで並べる形だが、複雑になりがちなので授業ではまず1つの親を継承するパターンに集中する。
継承の構文¶
子クラスを定義するときは、クラス名のあとに (親クラス名) を付ける。
たったこれだけで、親クラスの中身がまるごと子クラスに引き継がれる。
実例:Hero を Character から作る¶
まず親クラス Character を1つ作る。
class Character:
def __init__(self, name, hp):
self.name = name # ┐ インスタンス変数
self.hp = hp # ┘
def take_damage(self, damage):
self.hp = self.hp - damage
if self.hp < 0: # ← HP が 0 未満にならないように
self.hp = 0
print(self.name, 'の残りHP:', self.hp)
def show_status(self):
print(self.name, 'のHP:', self.hp)
次に、Character を継承して Hero を作る。
Hero には __init__ も show_status も書いていないのに、ちゃんと動く。
a = Hero('勇者', 100)
a.show_status() # → 勇者 のHP: 100 ← 親から継承
a.special() # → 勇者 の必殺技! ← Hero で追加
a.take_damage(30) # → 勇者 の残りHP: 70 ← 親から継承
プレ演習8-1¶
「まとめて動かす完全例」の Character / Hero / Wizard を打ち込んで、次の2つを動かそう(Wizard は本文の super() までを読んで定義したものを使う)。
期待される実行結果:
Hero クラスにはメソッドが1つしか書かれていないのに、なぜ take_damage や show_status が動くのか、自分の言葉で説明してみよう。
オーバーライド:親のメソッドを上書きする¶
子クラスで親と 同じ名前のメソッド を定義すると、子クラス側の定義が優先される。これを オーバーライド(override) という。
Wizard の show_status が呼ばれて、親の show_status は 隠される。take_damage は上書きしていないので親の定義がそのまま使われる。
super():子から親を呼ぶ¶
オーバーライドした子のメソッドの中から、わざわざ親のメソッドを呼びたいことがある。そのときに使うのが super()。
「今の自分のクラスの 親 のメソッドを呼ぶ」という意味。
一番典型的な使い方は __init__ の中。Wizard の __init__ で name hp を自分でコピペするのは、結局親クラスの __init__ と同じことをしている。これは super().__init__() に任せられる。
class Wizard(Character):
def __init__(self, name, hp, mp):
super().__init__(name, hp) # ← 親の __init__ に name, hp を任せる
self.mp = mp # ← 自分で追加する部分だけ書く
こうすれば、もし将来 Character.__init__ に処理を足しても、Wizard 側を直す必要はない。
super は関数呼び出し
super.method() ではなく super().method()。super の後の () を忘れるとエラーになる。
super() とオーバーライドの合わせ技¶
show_status も同じパターンで書ける。「親と同じことをしてから、自分の追加処理をする」。
class Wizard(Character):
def __init__(self, name, hp, mp):
super().__init__(name, hp)
self.mp = mp
def show_status(self): # オーバーライド
super().show_status() # ← まず親のshow_status(HP表示)
print(self.name, 'のMP:', self.mp) # ← そのあとに自分の追加表示
親のメソッドを呼んだうえで、自分の処理を 足す という形。コピペせずに「親 + α」が表現できる。
プレ演習8-2¶
次のコードの出力を、実行する前に紙やメモ帳に書き出してから実行し、予測と合っているか確認しよう。
class Character:
def __init__(self, name, hp):
self.name = name
self.hp = hp
def show_status(self):
print(self.name, 'のHP:', self.hp)
class Wizard(Character):
def __init__(self, name, hp, mp):
super().__init__(name, hp)
self.mp = mp
def show_status(self):
super().show_status()
print(self.name, 'のMP:', self.mp)
a = Wizard('マーリン', 50, 80)
a.show_status()
ポイント
super()が何を指すか、本文の説明を見直そう。- オーバーライドした側で
super().メソッド()を呼ぶと何が起きるか思い出そう。
まとめて動かす完全例¶
ここまでの内容を全部つないで1つのファイルにすると次のようになる。
class Character:
def __init__(self, name, hp):
self.name = name
self.hp = hp
def take_damage(self, damage):
self.hp = self.hp - damage
if self.hp < 0:
self.hp = 0
print(self.name, 'の残りHP:', self.hp)
def show_status(self):
print(self.name, 'のHP:', self.hp)
class Hero(Character):
def special(self):
print(self.name, 'の必殺技!')
class Wizard(Character):
def __init__(self, name, hp, mp):
super().__init__(name, hp)
self.mp = mp
def show_status(self):
super().show_status()
print(self.name, 'のMP:', self.mp)
a = Hero('勇者', 100)
b = Wizard('マーリン', 50, 80)
a.show_status() # → 勇者 のHP: 100
a.special() # → 勇者 の必殺技!
b.show_status() # → マーリン のHP: 50 / マーリン のMP: 80
b.take_damage(20) # → マーリン の残りHP: 30(親から継承)
共通部分(name hp take_damage の基本処理)は Character に 1か所だけ 書かれている。修正したくなったら、そこだけ直せばよい。
プレ演習8-3¶
Character を継承して、新しいクラス Slime を作りたい。スライムは name と hp に加えて、固有のステータス color(スライムの色、文字列)を持つ。show_status は親の表示に続けて色も表示したい。空欄 ① ② を埋めて完成させよう。
class Slime(Character):
def __init__(self, name, hp, color):
①
self.color = color
def show_status(self):
②
print(self.name, 'の色:', self.color)
s = Slime('スライム', 20, '青')
s.show_status()
期待される実行結果:
プレ演習8-4¶
次のコードには 3か所の誤り がある。それぞれ見つけて、正しく動くように直そう。なお、Character はこれまでと同じものが定義されているとする。
class Wizard:
def __init__(self, name, hp, mp):
super().__init__(name)
self.mp = mp
def show_status(self):
super.show_status()
print(self.name, 'のMP:', self.mp)
a = Wizard('マーリン', 50, 80)
a.show_status()
期待される実行結果:
ヒント
継承の構文、親 Character.__init__ が要求する引数、super() の書き方を本文と照らし合わせよう。
プレ演習8-5¶
Character を継承して、新しいクラス Boss を作ろう。条件は次の通り。
__init__は 書かない(親のものをそのまま使う)。roar()というメソッドだけを追加する。roar()を呼ぶと<名前> が現れた!と表示する。
そのうえで以下を実行し、出力を確認しよう。
a = Boss('魔王', 200)
a.roar() # → 魔王 が現れた!
a.show_status() # → 魔王 のHP: 200(継承)
a.take_damage(80) # → 魔王 の残りHP: 120(継承)
__init__ を書かなくても Boss('魔王', 200) が動くのはなぜか、自分の言葉で説明してみよう。
もっと知りたい人へ¶
isinstance(obj, クラス):あるオブジェクトが、指定したクラス(や、その親クラス)のインスタンスかを判定する。isinstance(a, Hero)もisinstance(a, Character)も両方Trueになる。- 多重継承:
class C(A, B):のように親を複数指定できる。AとBの両方からメソッドを引き継ぐ。 - MRO(Method Resolution Order):多重継承で「同じ名前のメソッドがあったらどっちを使うか?」の探索順。
クラス名.__mro__で確認できる。複雑になりがちなので、まずは単一継承に慣れるのがおすすめ。
でも、こんな入力が来たら?¶
第7回で take_damage の中に if self.hp < 0: という門番を作った。HP がマイナスにならない、までは守れている。でも、こんな入力には対応できていない。
take_damage('つよい')はself.hp - 'つよい'で型エラーになり、プログラムごと落ちる。take_damage(-50)はそのまま通って、本来下がるはずの HP が逆に増えてしまう。
ふだん使うアプリが、おかしな入力に対しても「正しく入力してください」とだけ出して 落ちない のは、こういう状況を安全に捌く仕組みがあるから。次回はその仕組み、例外処理 を学ぶ。
授業内演習¶
次のプログラムにおいて、自己紹介文を返す introduce() メソッドを Person クラスに追加し、実行例のとおり出力するプログラムを作成せよ。
introduce(self)は'私の名前は<名前>です。'という文字列を 返す(printするのではなくreturnする)。
# 親クラス
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f'名前:{self.name} 年齢:{self.age}'
# 子クラス
class Student(Person):
def __init__(self, name, age, student_id, major):
super().__init__(name, age)
self.student_id = student_id
self.major = major
def __str__(self):
base = super().__str__()
return f'{base} 学籍番号:{self.student_id} 専攻:{self.major}'
a = Student('神大太郎', 20, 202400001, '電気電子情報工学')
print(a)
# 追加:introduce() を Person に書き、↓が動くようにする
print(a.introduce())
期待される実行結果:
__str__ とは
print(オブジェクト) で出力される文字列を決める特殊メソッド。Student 側で __str__ をオーバーライドしているので、print(a) は親と違う表示になる。super().__str__() で親の表示を取り出し、その後ろに学籍番号と専攻を足している。これも今回のオーバーライド + super() のパターン。
基本課題¶
上の授業内演習に、勉強内容を返す study() メソッドを Student クラスに追加し、実行例のとおり出力するプログラムを作成せよ。Person 側には手を加えないこと。
| 種別 | 名前 | 内容 | 型 |
|---|---|---|---|
| メソッド | study(self) |
'<専攻>を勉強しています。' という文字列を 返す |
str |
実行例:
約束ごと:
Personクラスは変更しないこと(introduceは授業内演習で追加した状態のままにする)。Student.studyは 引数を取らない(selfのみ)。インスタンス変数self.majorを使う。studyは文字列をreturnする(printではない)。
応用課題¶
基本課題の構成に、Person の子クラス Course を追加せよ。授業(コース)も「自己紹介する側」と見立て、Person を親として扱う。
| 種別 | 名前 | 内容 | 型 |
|---|---|---|---|
| インスタンス変数 | course_name |
授業名 | str |
| インスタンス変数 | room |
教室名 | str |
| インスタンス変数 | teacher |
担当教員名 | str |
| メソッド | __str__(self) |
'コース名:<授業名> 部屋:<教室名> 担当教員:<担当教員名>' を返す |
str |
| メソッド | study(self) |
'<授業名>を受講しています。' を返す |
str |
実行例:
student = Student('神大太郎', 20, 202400001, '電気電子情報工学')
course = Course('プロ言語II', '20-202', '田中先生')
print(student)
print(course)
print(student.introduce())
print(student.study())
print(course.study())
名前:神大太郎 年齢:20 学籍番号:202400001 専攻:電気電子情報工学
コース名:プロ言語II 部屋:20-202 担当教員:田中先生
私の名前は神大太郎です。
電気電子情報工学を勉強しています。
プロ言語IIを受講しています。
約束ごと:
Person/Studentクラスは変更しないこと。Courseはclass Course(Person):のかたちでPersonを継承すること(構文上Personの子クラスにする)。Course.__init__はcourse_name/room/teacherの 3 つを引数として受け取る(実行例の呼び出し方に合わせる)。CourseではPersonのname/ageを使わないので、super().__init__(...)は呼ばなくてよい。Course.__str__は独自の表示文字列を返してよい(実行例のとおりname/ageは表示しない)。- 外部モジュールのインポートは行わないこと。
ヒント:
study()の戻り値は本文のStudent.study()と同じ「文字列を返すだけ」のパターン。- 「親を継承しても、親の
__init__を必ず呼ばないといけないわけではない」ことに気付くと、設計の自由度が見える。