Python – OOP Part 5. 継承とサブクラス(Inheritance and Subclass)

読了 14分

今回の講座では、クラスの継承とサブクラスについて学んでいきます。

前回の講座で説明したように、クラスを使う理由は、名前空間を利用して効率的にデータを管理し、同じコードの繰り返しをなくそうとするDRY(Don’t Repeat Yourself)の概念が含まれています。継承もまた、一度定義したデータ型を必要に応じて修正し、再利用して繰り返されるコードを減らそうという目的を持っています。例題を通じて、ゲームキャラクターのクラスを作りながら説明していきます。作ろうとするキャラクターは次のとおりです。

属性
クラスHero
ランク英雄
ライフ300
サイズbig
説明上位ランクのキャラクターとして、下位ランクのキャラクターを召喚します。
属性
クラスGoblin
ランク兵士
ライフ100
サイズsmall
説明下位ランクのキャラクターとして、上位ランクのキャラクターによって召喚されます。

まず最初に、すべてのキャラクターが持つ属性を備え、すべてのクラスのベースとなる Unit クラスを作り、Unit クラスのすべての属性を継承するゴブリンのサブクラスを作ってみましょう。

oop_5.py
class Unit:
def __init__(self, rank, size, life):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life

    def show_status(self):
        print('名前: {}'.format(self.name))
        print('ランク: {}'.format(self.rank))
        print('サイズ: {}'.format(self.size))
        print('ライフ: {}'.format(self.life))


class Goblin(Unit):
pass


goblin_1 = Goblin('兵士', 'Small', 100)

goblin_1.show_status()
実行結果
名前: Goblin
ランク: 兵士
サイズ: Small
ライフ: 100

Unit クラスの下位クラスである Goblin クラスは、何らのメソッドや属性も定義していないにもかかわらず、Unit クラスの中に定義した show_status メソッドを継承して使うことができました。それでは Goblin クラスは自身の名前空間の中に show_status メソッドを持っているのでしょうか?下のコードをご覧ください。

oop_5.py
class Unit:
def __init__(self, rank, size, life):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life

    def show_status(self):
        print('名前: {}'.format(self.name))
        print('ランク: {}'.format(self.rank))
        print('サイズ: {}'.format(self.size))
        print('ライフ: {}'.format(self.life))


class Goblin(Unit):
pass


goblin_1 = Goblin('兵士', 'Small', 100)

print(Goblin.__dict__)
実行結果
{'__doc__': None, '__module__': '__main__'}

上の結果を見ると、Goblin クラスは show_status メソッドを実際には持っていません。それなら、どうやって呼び出したのでしょうか?help() 関数を利用して Goblin クラスがどのように show_status メソッドを見つけたのか確認してみます。

oop_5.py
class Unit:
def __init__(self, rank, size, life):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life

    def show_status(self):
        print('名前: {}'.format(self.name))
        print('ランク: {}'.format(self.rank))
        print('サイズ: {}'.format(self.size))
        print('ライフ: {}'.format(self.life))


class Goblin(Unit):
pass

print(help(Goblin))
実行結果
Help on class Goblin in module __main__:

class Goblin(Unit)
|  Goblin(rank, size, life)
|
|  Method resolution order:
|      Goblin
|      Unit
|      builtins.object
|
|  Methods inherited from Unit:
|
|  __init__(self, rank, size, life)
|      Initialize self.  See help(type(self)) for accurate signature.
|
|  show_status(self)
|
|  ----------------------------------------------------------------------
|  Data descriptors inherited from Unit:
|
|  __dict__
|      dictionary for instance variables (if defined)
|
|  __weakref__
|      list of weak references to the object (if defined)

None
|  Method resolution order:
|      Goblin
|      Unit
|      builtins.object

上の説明は、Goblin クラスが名前を探す順番を示しています。最初に自身の名前空間で探し、次に親クラスである Unit クラスの名前空間を探り、最後に最上位クラスである組み込みオブジェクトクラスの名前空間を参照していますね。つまり、インスタンス生成時に必ず呼び出される必要がある init() メソッドも、自身の名前空間で見つけられなければ親クラスから参照するということです。

|  Methods inherited from Unit:
|
|  __init__(self, rank, size, life)
|      Initialize self.  See help(type(self)) for accurate signature.
|
|  show_status(self)

上の説明を見ると、init()show_status() メソッドを Unit クラスから継承したと表示されていますね。結局、自身の名前空間の中には持っていないけれども、上位クラスの名前空間を参照しているということです。

dir() 関数を利用して、Goblin クラスが参照できる情報を確認してみます。

oop_5.py
class Unit:
def __init__(self, rank, size, life):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life

    def show_status(self):
        print('名前: {}'.format(self.name))
        print('ランク: {}'.format(self.rank))
        print('サイズ: {}'.format(self.size))
        print('ライフ: {}'.format(self.life))


class Goblin(Unit):
pass


print(dir(Goblin))
実行結果
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'show_status']

今度は、Goblin クラスに新しい属性である「攻撃タイプ」を追加しようと思います。

oop_5.py
class Unit:
def __init__(self, rank, size, life):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life

    def show_status(self):
        print('名前: {}'.format(self.name))
        print('ランク: {}'.format(self.rank))
        print('サイズ: {}'.format(self.size))
        print('ライフ: {}'.format(self.life))


class Goblin(Unit):
def __init__(self, rank, size, life, attack_type):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life
self.attack_type = attack_type

    def show_status(self):
        print('名前: {}'.format(self.name))
        print('ランク: {}'.format(self.rank))
        print('サイズ: {}'.format(self.size))
        print('ライフ: {}'.format(self.life))
        print('攻撃タイプ: {}'.format(self.attack_type))

goblin_1 = Goblin('兵士', 'Small', 100, '近接攻撃')

goblin_1.show_status()
実行結果
名前: Goblin
ランク: 兵士
サイズ: Small
ライフ: 100
攻撃タイプ: 近接攻撃

自クラスの中に init() メソッドと show_status() メソッドを再定義して属性と機能を追加しましたが、Unit クラスで定義したすべてのコードをそのまま繰り返してしまっています。このようなコーディングではクラスを使う意味がなく、避けるべきアンチパターンの代表例です。

このように上位クラスのメソッドを自身のクラスの中に再定義することをメソッドオーバーライドと言い、このような場合には super() 関数を使って、すでに親クラスで定義されたメソッドを呼び出して、同じコードを繰り返さないようにします。super() 関数を使って重複したコードを減らしてみます。

oop_5.py
class Unit:
def __init__(self, rank, size, life):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life

    def show_status(self):
        print('名前: {}'.format(self.name))
        print('ランク: {}'.format(self.rank))
        print('サイズ: {}'.format(self.size))
        print('ライフ: {}'.format(self.life))


class Goblin(Unit):
def __init__(self, rank, size, life, attack_type):
super(Goblin, self).__init__(rank, size, life)
self.attack_type = attack_type

    def show_status(self):
        super(Goblin, self).show_status()
        print('攻撃タイプ: {}'.format(self.attack_type))


goblin_1 = Goblin('兵士', 'Small', 100, '近接攻撃')

goblin_1.show_status()
実行結果
名前: Goblin
ランク: 兵士
サイズ: Small
ライフ: 100
攻撃タイプ: 近接攻撃

いかがですか?コードがとてもシンプルになったのではないでしょうか?これがメソッドオーバーライド(method override)です。

ゴブリンのクラスに攻撃メソッドとダメージ属性を追加します。そして Goblin クラスを継承する SphereGoblin クラスを作って sphere_type という属性を追加します。

oop_5.py
class Unit:
def __init__(self, rank, size, life):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life

    def show_status(self):
        print('名前: {}'.format(self.name))
        print('ランク: {}'.format(self.rank))
        print('サイズ: {}'.format(self.size))
        print('ライフ: {}'.format(self.life))


class Goblin(Unit):
# damage 属性追加
def __init__(self, rank, size, life, attack_type, damage):
super(Goblin, self).__init__(rank, size, life)
self.attack_type = attack_type
self.damage = damage

    def show_status(self):
        super(Goblin, self).show_status()
        print('攻撃タイプ: {}'.format(self.attack_type))
        # オーバーライドメソッド
        print('ダメージ: {}'.format(self.damage))

    # 攻撃メソッド追加
    def attack(self):
        print('[{}]が攻撃します! 相手のダメージ({})'.format(self.name, self.damage))


class SphereGoblin(Goblin):
def __init__(self, rank, size, life, attack_type, damage, sphere_type):
super(SphereGoblin, self).__init__(rank, size, life, attack_type, damage)
self.sphere_type = sphere_type

    def show_status(self):
        super(SphereGoblin, self).show_status()
        print('槍タイプ: {}'.format(self.sphere_type))


sphere_goblin_1 = SphereGoblin('兵士', 'Small', 100, 'レンジ攻撃', 10, '長い槍')

sphere_goblin_1.show_status()
実行結果
名前: SphereGoblin
ランク: 兵士
サイズ: Small
ライフ: 100
攻撃タイプ: レンジ攻撃
ダメージ: 10
槍タイプ: 長い槍

SphereGoblin クラスは、親クラスである Goblin クラスと祖父クラスである Unit クラスからすべての属性とメソッドを継承していることが分かります。

今度は、Goblin キャラクターを率いる Hero クラスを作っていきます。

oop_5.py
class Unit:
def __init__(self, rank, size, life):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life

    def show_status(self):
        print('名前: {}'.format(self.name))
        print('ランク: {}'.format(self.rank))
        print('サイズ: {}'.format(self.size))
        print('ライフ: {}'.format(self.life))


class Goblin(Unit):
# damage 属性追加
def __init__(self, rank, size, life, attack_type, damage):
super(Goblin, self).__init__(rank, size, life)
self.attack_type = attack_type
self.damage = damage

    def show_status(self):
        super(Goblin, self).show_status()
        print('攻撃タイプ: {}'.format(self.attack_type))
        # オーバーライドメソッド
        print('ダメージ: {}'.format(self.damage))

    # 攻撃メソッド追加
    def attack(self):
        print('[{}]が攻撃します! 相手のダメージ({})'.format(self.name, self.damage))


class SphereGoblin(Goblin):
def __init__(self, rank, size, life, attack_type, damage, sphere_type):
super(SphereGoblin, self).__init__(rank, size, life, attack_type, damage)
self.sphere_type = sphere_type

    def show_status(self):
        super(SphereGoblin, self).show_status()
        print('槍タイプ: {}'.format(self.sphere_type))


class Hero(Unit):
def __init__(self, rank, size, life, goblins=None):
super(Hero, self).__init__(rank, size, life)
if goblins is None:
self.goblins = []
else:
self.goblins = goblins

    def show_own_goblins(self):
        num_of_goblins = len([x for x in self.goblins if isinstance(x, Goblin)])
        num_of_sphere_goblins = len([x for x in self.goblins if isinstance(x, SphereGoblin)])
        print('現在ヒーローが所有しているゴブリンは{}体、槍ゴブリンは{}体です。'.format(num_of_goblins, num_of_sphere_goblins))

    def make_goblins_attack(self):
        for goblin in self.goblins:
            goblin.attack()


# ゴブリンオブジェクト生成
goblin_1 = Goblin('兵士', 'Small', 100, '近接攻撃', 15)
goblin_2 = Goblin('兵士', 'Small', 100, '近接攻撃', 15)
sphere_goblin_1 = SphereGoblin('兵士', 'Small', 100, 'レンジ攻撃', 10, '長い槍')

# ヒーローオブジェクト生成後、ゴブリンオブジェクト割り当て
hero_1 = Hero('英雄', 'Big', 300, [goblin_1, goblin_2, sphere_goblin_1])
hero_1.show_own_goblins()
hero_1.make_goblins_attack()
実行結果
現在ヒーローが所有しているゴブリンは3体、槍ゴブリンは1体です。
[Goblin]が攻撃します! 相手のダメージ(15)
[Goblin]が攻撃します! 相手のダメージ(15)
[SphereGoblin]が攻撃します! 相手のダメージ(10)

最上位のクラスである Unit クラスから継承を受ける Hero クラスを作り、__init__() をオーバーライドしました。そして、自分が所有しているゴブリンの数を表示するメソッドと、所有しているゴブリンに攻撃をさせるメソッドも作ってみました。今度は、ゴブリンを追加したり削除したりするメソッドを作っていきます。

oop_5.py
class Unit:
def __init__(self, rank, size, life):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life

    def show_status(self):
        print('名前: {}'.format(self.name))
        print('ランク: {}'.format(self.rank))
        print('サイズ: {}'.format(self.size))
        print('ライフ: {}'.format(self.life))


class Goblin(Unit):
# damage 属性追加
def __init__(self, rank, size, life, attack_type, damage):
super(Goblin, self).__init__(rank, size, life)
self.attack_type = attack_type
self.damage = damage

    def show_status(self):
        super(Goblin, self).show_status()
        print('攻撃タイプ: {}'.format(self.attack_type))
        # オーバーライドメソッド
        print('ダメージ: {}'.format(self.damage))

    # 攻撃メソッド追加
    def attack(self):
        print('[{}]が攻撃します! 相手のダメージ({})'.format(self.name, self.damage))


class SphereGoblin(Goblin):
def __init__(self, rank, size, life, attack_type, damage, sphere_type):
super(SphereGoblin, self).__init__(rank, size, life, attack_type, damage)
self.sphere_type = sphere_type

    def show_status(self):
        super(SphereGoblin, self).show_status()
        print('槍タイプ: {}'.format(self.sphere_type))


class Hero(Unit):
def __init__(self, rank, size, life, goblins=None):
super(Hero, self).__init__(rank, size, life)
if goblins is None:
self.goblins = []
else:
self.goblins = goblins

    def show_own_goblins(self):
        num_of_goblins = len([x for x in self.goblins if isinstance(x, Goblin)])
        num_of_sphere_goblins = len([x for x in self.goblins if isinstance(x, SphereGoblin)])
        print('現在ヒーローが所有しているゴブリンは{}体、槍ゴブリンは{}体です。'.format(
            num_of_goblins - num_of_sphere_goblins, num_of_sphere_goblins
        ))

    def make_goblins_attack(self):
        for goblin in self.goblins:
            goblin.attack()

    def add_goblins(self, new_goblins):
        for goblin in new_goblins:
            if goblin not in self.goblins:
                self.goblins.append(goblin)
            else:
                print('すでに追加されているゴブリンです。')

    def remove_goblins(self, old_goblins):
        for goblin in old_goblins:
            try:
                self.goblins.remove(goblin)
            except:
                print('{}を所有していません。'.format(goblin))


# ゴブリンオブジェクト生成
goblin_1 = Goblin('兵士', 'Small', 100, '近接攻撃', 15)
goblin_2 = Goblin('兵士', 'Small', 100, '近接攻撃', 15)
sphere_goblin_1 = SphereGoblin('兵士', 'Small', 100, 'レンジ攻撃', 10, '長い槍')

# ヒーローオブジェクト生成後、ゴブリンオブジェクト割り当て
hero_1 = Hero('英雄', 'Big', 300, [goblin_1, goblin_2, sphere_goblin_1])

# 新しいゴブリン生成
goblin_3 = Goblin('兵士', 'Small', 100, '近接攻撃', 20)
sphere_goblin_2 = SphereGoblin('兵士', 'Small', 100, 'レンジ攻撃', 5, '長い槍')

print('# 新しいゴブリン追加前')
hero_1.show_own_goblins()
hero_1.make_goblins_attack()

# 新しいゴブリン追加
hero_1.add_goblins([goblin_3, sphere_goblin_2])

print('\n# 新しいゴブリン追加後')
hero_1.show_own_goblins()
hero_1.make_goblins_attack()

# 追加したゴブリン削除
hero_1.remove_goblins([goblin_3, sphere_goblin_2])

print('\n# 追加したゴブリン削除後')
hero_1.show_own_goblins()
hero_1.make_goblins_attack()
実行結果
# 新しいゴブリン追加前
現在ヒーローが所有しているゴブリンは2体、槍ゴブリンは1体です。
[Goblin]が攻撃します! 相手のダメージ(15)
[Goblin]が攻撃します! 相手のダメージ(15)
[SphereGoblin]が攻撃します! 相手のダメージ(10)

# 新しいゴブリン追加後
現在ヒーローが所有しているゴブリンは3体、槍ゴブリンは2体です。
[Goblin]が攻撃します! 相手のダメージ(15)
[Goblin]が攻撃します! 相手のダメージ(15)
[SphereGoblin]が攻撃します! 相手のダメージ(10)
[Goblin]が攻撃します! 相手のダメージ(20)
[SphereGoblin]が攻撃します! 相手のダメージ(5)

# 追加したゴブリン削除後
現在ヒーローが所有しているゴブリンは2体、槍ゴブリンは1体です。
[Goblin]が攻撃します! 相手のダメージ(15)
[Goblin]が攻撃します! 相手のダメージ(15)
[SphereGoblin]が攻撃します! 相手のダメージ(10)

すでに追加されているゴブリンを追加したり、所有していないゴブリンを削除したりして、エラーメッセージを発生させてみましょう。

oop_5.py
class Unit:
def __init__(self, rank, size, life):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life

    def show_status(self):
        print('名前: {}'.format(self.name))
        print('ランク: {}'.format(self.rank))
        print('サイズ: {}'.format(self.size))
        print('ライフ: {}'.format(self.life))


class Goblin(Unit):
# damage 属性追加
def __init__(self, rank, size, life, attack_type, damage):
super(Goblin, self).__init__(rank, size, life)
self.attack_type = attack_type
self.damage = damage

    def show_status(self):
        super(Goblin, self).show_status()
        print('攻撃タイプ: {}'.format(self.attack_type))
        # オーバーライドメソッド
        print('ダメージ: {}'.format(self.damage))

    # 攻撃メソッド追加
    def attack(self):
        print('[{}]が攻撃します! 相手のダメージ({})'.format(self.name, self.damage))


class SphereGoblin(Goblin):
def __init__(self, rank, size, life, attack_type, damage, sphere_type):
super(SphereGoblin, self).__init__(rank, size, life, attack_type, damage)
self.sphere_type = sphere_type

    def show_status(self):
        super(SphereGoblin, self).show_status()
        print('槍タイプ: {}'.format(self.sphere_type))


class Hero(Unit):
def __init__(self, rank, size, life, goblins=None):
super(Hero, self).__init__(rank, size, life)
if goblins is None:
self.goblins = []
else:
self.goblins = goblins

    def show_own_goblins(self):
        num_of_goblins = len([x for x in self.goblins if isinstance(x, Goblin)])
        num_of_sphere_goblins = len([x for x in self.goblins if isinstance(x, SphereGoblin)])
        print('現在ヒーローが所有しているゴブリンは{}体、槍ゴブリンは{}体です。'.format(num_of_goblins, num_of_sphere_goblins))

    def make_goblins_attack(self):
        for goblin in self.goblins:
            goblin.attack()

    def add_goblins(self, new_goblins):
        for goblin in new_goblins:
            if goblin not in self.goblins:
                self.goblins.append(goblin)
            else:
                print('すでに追加されているゴブリンです。')

    def remove_goblins(self, old_goblins):
        for goblin in old_goblins:
            try:
                self.goblins.remove(goblin)
            except:
                print('所有していないゴブリンです。')


# ゴブリンオブジェクト生成
goblin_1 = Goblin('兵士', 'Small', 100, '近接攻撃', 15)
goblin_2 = Goblin('兵士', 'Small', 100, '近接攻撃', 15)
sphere_goblin_1 = SphereGoblin('兵士', 'Small', 100, 'レンジ攻撃', 10, '長い槍')

# ヒーローオブジェクト生成後、ゴブリンオブジェクト割り当て
hero_1 = Hero('英雄', 'Big', 300, [goblin_1, goblin_2, sphere_goblin_1])

# 新しいゴブリン生成
goblin_3 = Goblin('兵士', 'Small', 100, '近接攻撃', 20)
sphere_goblin_2 = SphereGoblin('兵士', 'Small', 100, 'レンジ攻撃', 5, '長い槍')

print('# 新しいゴブリン追加前')
hero_1.show_own_goblins()
hero_1.make_goblins_attack()

# 新しいゴブリン追加
hero_1.add_goblins([goblin_3, sphere_goblin_2])

print('\n# 新しいゴブリン追加後')
hero_1.show_own_goblins()
hero_1.make_goblins_attack()

# 追加したゴブリン削除
hero_1.remove_goblins([goblin_3, sphere_goblin_2])

print('\n# 追加したゴブリン削除後')
hero_1.show_own_goblins()
hero_1.make_goblins_attack()

# ヒーローに所有されていないゴブリン生成
goblin_4 = Goblin('兵士', 'Small', 100, '近接攻撃', 20)

# すでに所有しているゴブリン追加
print('\n# エラーメッセージ発生')
hero_1.add_goblins([goblin_1])
hero_1.remove_goblins([goblin_4])
実行結果
# 新しいゴブリン追加前
現在ヒーローが所有しているゴブリンは3体、槍ゴブリンは1体です。
[Goblin]が攻撃します! 相手のダメージ(15)
[Goblin]が攻撃します! 相手のダメージ(15)
[SphereGoblin]が攻撃します! 相手のダメージ(10)

# 新しいゴブリン追加後
現在ヒーローが所有しているゴブリンは5体、槍ゴブリンは2体です。
[Goblin]が攻撃します! 相手のダメージ(15)
[Goblin]が攻撃します! 相手のダメージ(15)
[SphereGoblin]が攻撃します! 相手のダメージ(10)
[Goblin]が攻撃します! 相手のダメージ(20)
[SphereGoblin]が攻撃します! 相手のダメージ(5)

# 追加したゴブリン削除後
現在ヒーローが所有しているゴブリンは3体、槍ゴブリンは1体です。
[Goblin]が攻撃します! 相手のダメージ(15)
[Goblin]が攻撃します! 相手のダメージ(15)
[SphereGoblin]が攻撃します! 相手のダメージ(10)

# エラーメッセージ発生
すでに追加されているゴブリンです。
所有していないゴブリンです。

エラーメッセージも問題なく出力されますね。

本記事ではクラスの継承とサブクラスの作成方法を整理しました。プログラミングは文章を読んで頭で理解するだけでなく、実際にコーディングし、問題に遭遇しながら手を動かして覚えていくことが大切です。さまざまなクラスを作りながら、どんなデータをクラスとして表現できるかを検討してみてください。

次回はクラスが提供するマジックメソッドを取り上げます。

X