コンテンツにスキップ

第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 クラスを継承する。strlistint も、たどっていけばすべて object の子クラス。

print(int.__mro__)
# → (<class 'int'>, <class 'object'>)

ふだん意識しないが、int+ で足し算できたり str.upper() を持てるのは、こうしたクラスの仕組みのおかげ。

なお Python は 多重継承(親を複数指定する)も書ける。class C(A, B): のようにカンマで並べる形だが、複雑になりがちなので授業ではまず1つの親を継承するパターンに集中する。

継承の構文

子クラスを定義するときは、クラス名のあとに (親クラス名) を付ける。

class 子クラス名(親クラス名):
    # 親にないものだけ書く(あるいは親のものを上書きする)
    ...

たったこれだけで、親クラスの中身がまるごと子クラスに引き継がれる。

実例: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 を作る。

class Hero(Character):
    def special(self):
        print(self.name, 'の必殺技!')

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() までを読んで定義したものを使う)。

a = Hero('勇者', 100)
a.special()

b = Wizard('魔法使い', 50, 80)
b.show_status()

期待される実行結果:

勇者 の必殺技!
魔法使い のHP: 50
魔法使い のMP: 80

Hero クラスにはメソッドが1つしか書かれていないのに、なぜ take_damageshow_status が動くのか、自分の言葉で説明してみよう。

オーバーライド:親のメソッドを上書きする

子クラスで親と 同じ名前のメソッド を定義すると、子クラス側の定義が優先される。これを オーバーライド(override) という。

class Wizard(Character):
    def show_status(self):                          # ← 親と同じ名前で定義
        print(self.name, 'は魔法使いだ!')
a = Wizard('マーリン', 50)
a.show_status()    # → マーリン は魔法使いだ!(子が優先)

Wizardshow_status が呼ばれて、親の show_status隠されるtake_damage は上書きしていないので親の定義がそのまま使われる。

super():子から親を呼ぶ

オーバーライドした子のメソッドの中から、わざわざ親のメソッドを呼びたいことがある。そのときに使うのが 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)            # ← そのあとに自分の追加表示
a = Wizard('マーリン', 50, 80)
a.show_status()
# → マーリン のHP: 50
# → マーリン のMP: 80

親のメソッドを呼んだうえで、自分の処理を 足す という形。コピペせずに「親 + α」が表現できる。

プレ演習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 の基本処理)は Character1か所だけ 書かれている。修正したくなったら、そこだけ直せばよい。

プレ演習8-3

Character を継承して、新しいクラス Slime を作りたい。スライムは namehp に加えて、固有のステータス 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()

期待される実行結果:

スライム のHP: 20
スライム の色: 青

プレ演習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()

期待される実行結果:

マーリン のHP: 50
マーリン のMP: 80
ヒント

継承の構文、親 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): のように親を複数指定できる。AB の両方からメソッドを引き継ぐ。
  • MRO(Method Resolution Order):多重継承で「同じ名前のメソッドがあったらどっちを使うか?」の探索順。クラス名.__mro__ で確認できる。複雑になりがちなので、まずは単一継承に慣れるのがおすすめ。

でも、こんな入力が来たら?

第7回で take_damage の中に if self.hp < 0: という門番を作った。HP がマイナスにならない、までは守れている。でも、こんな入力には対応できていない。

a = Hero('勇者', 100)
a.take_damage('つよい')   # ← 数字じゃなく文字列!
a.take_damage(-50)        # ← マイナス(= 回復してしまう)
  • 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())

期待される実行結果:

名前:神大太郎 年齢:20 学籍番号:202400001 専攻:電気電子情報工学
私の名前は神大太郎です。

__str__ とは

print(オブジェクト) で出力される文字列を決める特殊メソッド。Student 側で __str__ をオーバーライドしているので、print(a) は親と違う表示になる。super().__str__() で親の表示を取り出し、その後ろに学籍番号と専攻を足している。これも今回のオーバーライド + super() のパターン。

基本課題

上の授業内演習に、勉強内容を返す study() メソッドを Student クラスに追加し、実行例のとおり出力するプログラムを作成せよ。Person 側には手を加えないこと。

種別 名前 内容
メソッド study(self) '<専攻>を勉強しています。' という文字列を 返す str

実行例:

a = Student('神大太郎', 20, 202400001, '電気電子情報工学')
print(a)
print(a.introduce())
print(a.study())
名前:神大太郎 年齢:20 学籍番号:202400001 専攻:電気電子情報工学
私の名前は神大太郎です。
電気電子情報工学を勉強しています。

約束ごと:

  • 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 クラスは変更しないこと。
  • Courseclass Course(Person): のかたちで Person を継承すること(構文上 Person の子クラスにする)。
  • Course.__init__course_name / room / teacher の 3 つを引数として受け取る(実行例の呼び出し方に合わせる)。Course では Personname / age を使わないので、super().__init__(...) は呼ばなくてよい。
  • Course.__str__ は独自の表示文字列を返してよい(実行例のとおり name/age は表示しない)。
  • 外部モジュールのインポートは行わないこと。

ヒント:

  • study() の戻り値は本文の Student.study() と同じ「文字列を返すだけ」のパターン。
  • 「親を継承しても、親の __init__ を必ず呼ばないといけないわけではない」ことに気付くと、設計の自由度が見える。