Hyper-Positive-Diary

TWICE/遊戯王/バイオインフォ/読書記録/アニメの感想等を走り書きします。

【Python】遊戯王で理解するクラスの継承

~ Pythonを勉強中だけど、クラスがわからない、そんなあなたへ ~

本記事は、以前に書いた記事、 (【Python遊戯王で理解するオブジェクト指向)の第2段に当たります。 slmnphmet1-2.hatenablog.com

インスタンス変数が「カードの基本情報」に、メソッドが「カードの効果」に対応していることは、前回ご紹介したかと思います。 今回は、クラスを継承するとは何か、ということを、またも遊戯王を引き合いに出しながら確認していきます。

Pythonを勉強中だけどクラスってよくわからないんだよね〜」という人、 もしくは、クラスはわかってきたんだけど、継承の意味やメリットがわからない!という人が、 少しでもイメージをつかんでくれたら嬉しいです。

継承とは、 カードの種類を指定すること

継承とは、親クラス(カードの種類)の情報を、子クラス(個々のカード)に渡すことだと思うとわかりやすいでしょう。 今回は、強欲な壺などの魔法カードを例に考えていきたいと思います。

「魔法カード」をクラスで表現してみる

遊戯王において、魔法カードは主に手札から発動し、効果の適用が終わったら墓地に置かれます(永続魔法や装備魔法などは一旦考えないことにします)。 pythonの言葉で書くと、以下のようにすれば良いでしょう。

class Magic :
    def __init__(self) :
        self.cardtype = "魔法カード"
        self.name = None

    def activate(self) :
        hands.remove(self.name)
        self.effect()
        cemetary.append(self.name)

    def effect(self) :
        pass

Magicには、メソッドが二つ定義してあります。 activate(発動)では、手札(hands というリスト)から自分自身(self.name)を取り除き、 効果を適用(self.effect)し終えたら墓地(cemetary)に置くという意味です。

インスタンス変数のself.name とメソッドのeffectには、現状何も書かれていません。 なぜならば、定義したい魔法カードごとに名前や効果が異なるためです。 逆に言えば、self.nameとeffectさえ定義すれば、(activateをいちいち書き直さなくても)好きな魔法カードが定義できることになります。 以下で具体的に見てみましょう。

魔法カードのルールを継承させてみる

例として、魔法カードクラス(Magic)を継承し、強欲な壺(英名 : Greedy Pot)というクラスを定義してみることにします。

遊戯王 強欲な壺 107-044 ノーマル 【ランクA】 【中古】

class GreedyPot(Magic) :
    def __init__(self) :
        self.name = "強欲な壺"

    def effect(self) :
        hands.append(decks[0])
        decks.remove(decks[0])

        hands.append(decks[0])
        decks.remove(decks[0])

見ての通り、強欲な壺(GreedyPot)というクラスには、発動(activate)というメソッドが書かれていません。 しかし、魔法カードであることを指定(Magic クラスを継承)しているので、発動(activate)の処理を行うことができます。 activateの中ではeffectを行う(効果を適用する)処理があります。強欲なツボの効果(effect)は2枚ドローですので、 デッキ(配列 decks)から、カード2枚を取り出し、手札(配列 hands)にその二枚を加えます。

それでは、実際に強欲な壺というカードを発動(card.activate())してみましょう。 デッキにカードが2枚、手札に強欲なツボがあるとき、activateの後にどうなるかというと...

decks = ["グレムリン", "ブラック・マジシャン"]
hands = ["強欲な壺"]
cemetary = []

card = GreedyPot()
card.activate()

print(f"手札 : {hands}")
print(f"デッキ : {decks}")
print(f"墓地 : {cemetary}")
手札 : ['グレムリン', 'ブラック・マジシャン']
デッキ : []
墓地 : ['強欲な壺']

手札が2枚増えて、墓地に強欲な壺が置かれるはずです。

同じように、成金ゴブリン(英名 : Upstart Goblin)というカードを定義してみましょう。 (ライフ回復効果は、今回省きます。)

遊戯王 RC02-JP043 スーパーレア 魔法 成金ゴブリン 【中古】【Sランク】

class UpstartGoblin(Magic) :
    def __init__(self) :
        self.name = "成金ゴブリン"

    def effect(self) :
        hands.append(decks[0])
        decks.remove(decks[0])

やはり、逐一activateを書かなくて良いので、手間が省ける上、コードが見やすくなりますね。

decks = ["グレムリン", "ブラック・マジシャン"]
hands = ["成金ゴブリン"]
cemetary = []

card = UpstartGoblin()
card.activate()

print(f"手札 : {hands}")
print(f"デッキ : {decks}")
print(f"墓地 : {cemetary}")

ちなみに発動後はこんな感じです。

手札 : ['グレムリン']
デッキ : ['ブラック・マジシャン']
墓地 : ['成金ゴブリン']

このように、親クラス(Magic)を継承することで、親と共通する処理(activate)を省き、 コードが冗長になるのを防ぐことができます。Magicクラスを継承したときに、 魔法カードとしてのルールが適用されるんだということが、 コードを見る人にも一目瞭然ですね。

super()により、「魔法カード」としての基本情報を継承

しかしこのままでは、継承しきれていないものがあります。それは、親クラス(Magic)のインスタンス変数です。

親クラスのMagicには、インスタンス変数として、cardtypeが定義されていました。これを呼び出せば、「魔法カード」と出力されます。

magic = Magic()
print(magic.cardtype)

しかし、継承後に新しくコンストラクタを定義すると、この情報が失われます。 以下はエラーになります。

card = GreedyPot()
print(card.cardtype)

強欲な壺は魔法カードですから、cardtypeにアクセスしたときに、「魔法カード」と出力されて欲しいですよね。 そこで、super()の出番です。

class GreedyPot(Magic) :
    def __init__(self) :
        super().__init__()
        self.name = "強欲なツボ"

    def effect(self) :
        hands.append(decks[0])
        decks.remove(decks[0])

        hands.append(decks[0])
        decks.remove(decks[0])

card = GreedyPot()
print(card.cardtype)

今度は正しく「魔法カード」と出力されるはずです。 def init(self)直下に、super().init()を置いてやれば、 新しくコンストラクタを定義したときでも、 親クラス(Magic)のカードのインスタンス変数(カードの基本情報) を引き継ぐことができます。

まとめ

クラスの継承とは、カードの種類を指定し、そのルールを子クラス(個々のカード)に適用させることと対応しています。 また、super()は、継承後にも親クラスの情報を引き継ぐのに用いられます。 クラスの継承によって、冗長なコードが回避でき、複数の子クラス(個々のカード)を定義するときに見やすくなりますね。

遊戯王に例えると、イメージを掴みやすくなる分、正確さを欠きます。概観が掴めたという人は、是非オブジェクト指向の厳密な理解に励んでください。

最後に、参考になるサイトのリンクを貼っておきます。一読いただき、ありがとうございました。

python-works.hatenablog.com memopy.hatenadiary.jp qiita.com