見出し画像

pythonでクラスの継承と戦う

これを勉強してからもう2年も経つそうです……

2年前に作ったプログラムの改修をしようと思い立ち、pythonでもオブジェクト指向っぽい書き方したいな~って思ったので、改めて継承の実践的な使い方を整理していきたいと思います。

改めて、クラスを使うと何が便利か

「クラスを使うと、データと、それにまつわる処理をクラスの中にぎゅぎゅっと押し込んでしまえます。そうすれば、改修するときもどこを直すかわかりやすいし、使う時も使う側のコードがスッキリして良いよね!」

じゃ、クラスの継承を使うと何が便利か

使い回せるところは最大限使い回して、差分のところだけ書いておけばいいようにしたい。そんなときに活躍するのが継承です。

たとえば、勇者がいて、盗賊がいて、戦士がいて、魔法使いがいます。これらのキャラクター達についての「データ」と「処理」をひとまとめにしようと思います。

例えば、必要な「データ」はこんな感じ。

名前・レベル・HP・MP・使える魔法のリスト・持っているアイテムのリスト・現在の経験値
。。。。その他色々。

必要な「処理」はこんな感じ。

・現在の経験値>レベルアップに必要な経験値になったら、レベルを上げる処理
・モンスターから攻撃を受けたら、ダメージ値を計算して、HPを減らす処理
・魔法を使ったら、MPを減らす処理
。。。その他色々。

とりあえず、Characterっていうクラス(設計図・いわばキャラクターシートのテンプレート)を作っておいて、キャラクターを作ったらその都度新しいインスタンス(実体・実際にステータスを書き込んだキャラクターシート)を作っていけば、かなり楽に管理できそうです。

ところが、こんな仕様を入れたいとします。

・盗賊は一定の確率で相手の攻撃を避ける事ができる
・魔法使いは、MPの消費が他の職業の8割でいい
・勇者は、レベルアップに必要な経験値が他の職業より多い

こうなると、Characterクラスの各処理に、「もし盗賊なら……」「もし魔法使いなら……」「勇者なら……」という分岐を入れないといけない。

ifが増えるとバグが増えるので、if増やしたくない。

そこで、「Characterクラスと同じデータと処理が入ってるけど、処理の内容がちょっとだけ違う各職業用のクラスを用意すれば良かろう」ということになります。

そこで、Heroクラス、Thiefクラス、Magicianクラス…………って作っていくんですが、その時、全部のクラスに、

なまえ・HP・MP…………レベルアップ処理・ダメージ処理…………

って、一から作ってたら、コーディングも大変だし保守で死にます。

なので、使い回せるところは最大限使い回して、差分のところだけ書いておけばいいようにしたい。そんなときに活躍するのが継承です。

まずは親クラスを作る

class Character():
    def __init__(self, name):
        self.name = name
        self.level = 1
        self.hp = self.__calcHp()
        self.mp = self.__calcMp()
    
    def __calcHp(self):
        return self.level * 10
    
    def __calcMp(self):
        return self.level * 5
    
    def calcDamage(self, damage):
        return self.hp - damage

使い回す元となる汎用的なクラスを「親クラス」と呼びます。

こちらはとりあえず、何も考えずクラスを作っていきます。データの入れものを用意しておいて、そのデータを操作するためのメソッドを突っ込みます。

今回はダメージ値計算があるのでイミュータブルにはできませんが、一応、イミュータブル(変更不可)な雰囲気で作りたいなぁと思いながら、名前なんかはコンストラクタで受け取って、インスタンス変数に入れるような作りにしてあります。(上記のサンプルはサンプルなので色々と「それでええんか」ポイントがありますがまあ目を瞑って頂くとして……)

次に、子クラスを作る

使い回し先となるのが「子クラス」です。例えば、Thiefクラスを作ってみます。

from character import Character

class Thief(Character):
    def __init__(self, name):
        super().__init__(name)

まず、Characterクラスをimportします。(Javaならimportいらないけど、pythonだと必要みたい)

そして、class クラス名(): の()の中に、親クラス名を書いておきます。

これでひとまず、「Characterクラスに入ってる変数とメソッド全部持ってる子クラスThief」が完成です。

親クラスのコンストラクタを呼び出す

なんですが、Characterクラスではコンストラクタが定義されているので、このままだと、Characterクラスのコンストラクタ内で行われている処理が行われず、今回のケースで言えばnameとかhpなんかのインスタンス変数が作られません。

Characterクラスはコンストラクタの引数にnameが必要なので、Thiefのコンストラクタもnameを引数に取るようにします。そして、

super().__init__(name)

を実行してあげることで、親クラスのコンストラクタが実行されます。やったね。

メソッドをオーバーライドする

このままだと、ThiefクラスはCharacterクラスと全く同じ動作しかしません。それならCharacterクラスのままで良いことになりますので、Thief独特の処理を反映させます。

「盗賊は一定の確率で攻撃を避ける」のところを、ダメージ計算メソッドに実装してみます。

from character import Character

class Thief(Character):
    def __init__(self, name):
        super().__init__(name)

    def calcDamage(self, damage):
        if (self.__isMiss):
            return self.hp
        return self.hp - damage
        
    def __isMiss(self):
        # 乱数を生成してランダムに成功か失敗を判断するメソッド

肝心の部分はダミーですが今回の本題はそこじゃないのでご勘弁。

親クラスと同じ名前のメソッドを作ると、子クラスの処理が優先して使用されます(オーバーライド)。これを使えば、「このメソッドだけ他のクラスとは違う処理にしたい」という部分だけを簡単に書くことができます。

親クラスにはない新しいメソッドを追加することももちろん可能です。

親クラスで生成されるインスタンス変数へのアクセスはself.でOK

ところで、上記のサンプル、self.hpを使っていますが、Thiefクラス内ではインスタンス変数hpを宣言していません。これは、親クラスのコンストラクタが宣言しています。

しかし、コンストラクタを実行したときにちゃんとThiefクラス内に作られているので、self.を付けてアクセスすることが可能です。

さらに多重継承のことを考え出すとまた注意点が増えるのですが、とりあえず今回は基本的なところだけ。


この記事が気に入ったらサポートをしてみませんか?