0
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ドラクエによるクラス解説 for Python

Posted at

背景

いくつかの記事を拝見し、断片的な記事があるものの、実用性の観点からあまり見かけなかったため、今までの実務から鑑みてまとめていきます。

目的

Pythonクラスを使う時、初心者向けに以下の疑問を解消したい。

  1. どうしてクラスを使うの?(使わないとどうなる?)
  2. なぜわざわざクラス継承を使うの?(使わないとどうなる?)
  3. クラス記述する時に、selfをつけるのって、面倒くさくて、なんのメリットがあるの?
  4. ラッパーってなんに?(クラスがあるのになぜラッパー?)
  5. インスタンスってなに?(これがないとだめ?)
  6. クラスメソッドとクラス変数の違いってなに?
  7. 「if _name_ == '_main_':」というおまじない?

以下の内容は初心者向けではないので、対象外とする

  1. 多重継承
  2. イテレータ (iterator)
  3. ジェネレータ (generator)
  4. スコープと名前空間(global)
    ※マルチプロセス制御、モーター制御などで使われる

どうしてクラスを使うの?(使わないとどうなる?)

・開発現場において、分業化が進んでいるため、複数人がチームを作って、それぞれ別々の機能の実装を担当している。そのため、共通機能と個別機能の設計書を作成してから実装することになっている場合が多い。
・設計段階では、共通機能設計書と個別機能設計書が、それぞれ、共通クラス、個別クラスとして実装される

クラスを使わないとどうなるの?

  1. オブジェクト指向プログラミングの機能の欠如: クラスを使用しない場合、オブジェクト指向プログラミングの機能(継承、カプセル化、ポリモーフィズムなど)を利用することができません。これにより、コードの再利用性や保守性が低下し、柔軟性が制限される可能性があります。

  2. コードの分割と整理が難しくなる: クラスを使用しない場合、関連する機能やデータをまとめて管理する方法が制限されます。これにより、大規模なプログラムを管理する際にコードの分割や整理が難しくなり、理解しやすい構造のプログラムを作成することが難しくなります。

  3. 変数や関数のグローバルスコープ化: クラスを使用しない場合、変数や関数は通常、グローバルスコープに定義されます。これにより、変数や関数がプログラムのどこからでもアクセス可能になり、コードの依存関係が複雑化し、バグの発生やデバッグが困難になる可能性があります。

  4. コードの再利用性の低下: クラスを使用しない場合、関数や手続きを使って同じ機能を実現する必要があります。これにより、同じ機能を複数の場所で再利用することが難しくなり、コードの冗長性が増加する可能性があります。

  5. プログラムの拡張性の低下: クラスを使用しない場合、新しい機能を追加する際にプログラム全体を修正する必要がある場合があります。これにより、プログラムの拡張性が低下し、変更や修正が容易ではなくなる可能性があります。

総括すると、クラスを使用しない場合、プログラムの保守性、再利用性、拡張性が低下する可能性があります。特に大規模なプロジェクトでは、クラスを使用してオブジェクト指向プログラミングの原則を適用することが推奨されます。

あまり考えたことがないが、やっかいということは伝わると思います
以下は、パーティーを表示する事例を示す。

# play_game_04.py
class Hero:
    def __init__(self, name, level, hp):
        self.name = name
        self.level = level
        self.hp = hp

    def show_status(self):
        print(f"名前: {self.name}, レベル: {self.level}, HP: {self.hp}")

# 勇者のパーティーを表現するリスト
party = []

# 勇者のインスタンスを作成してリストに追加
hero1 = Hero("勇者", 1, 100)
hero2 = Hero("戦士", 1, 120)
hero3 = Hero("魔法使い", 1, 70)
hero4 = Hero("僧侶", 1, 90)

party.append(hero1)
party.append(hero2)
party.append(hero3)
party.append(hero4)

# パーティーの勇者のステータスを表示
for hero in party:
    hero.show_status()

実行結果

$ python play_game_04.py 
名前: 勇者, レベル: 1, HP: 100
名前: 戦士, レベル: 1, HP: 120
名前: 魔法使い, レベル: 1, HP: 70
名前: 僧侶, レベル: 1, HP: 90

クラスを使わなくても良い場合はあるよ!

  1. スクレイピングなどプログラム機能が単一で、開発規模が小さい場合
  2. クラスをテストする業務に携わる場合
  3. ちょっとした機能を試す場合
  4. 手続き型プログラミングのスタイルにした場合
  5. 関数型プログラミングのスタイルにした場合

以下は、Pythonの組み込み関数を使うprint事例を示す。

print('Dragon Quest‼')  

なぜわざわざクラス継承を使うの?(使わないとどうなる?)

まずは、使わないとどうなるか、ChatGPTに聞いてみました。

・Pythonではクラス継承を使わなくてもプログラムを書くことができますが、クラス継承を使用しない場合、以下のようなことが起こります。

  1. コードの再利用性が低下する: クラス継承は、共通の振る舞いや属性を持つオブジェクトのグループを作成し、それらの共通の振る舞いや属性を親クラスに定義することで、コードの再利用性を高めます。継承を使用しない場合、同じ機能を持つオブジェクトを作成するたびに同じコードを再度書く必要があります。

  2. コードの保守性が低下する: 継承を使用しない場合、コードが冗長になり、同じ機能や属性が複数の場所で再定義される可能性があります。これにより、コードの保守性が低下し、変更が必要な場合に全てのコードを手動で更新する必要があります。

  3. オブジェクト指向プログラミングの原則に反する可能性がある: クラス継承は、オブジェクト指向プログラミングの重要な原則の1つである"DRY (Don't Repeat Yourself)" 原則を支援します。継承を使用しない場合、同じコードが複数の場所に存在する可能性があり、これはDRY原則に反することになります。要は同じコードを書かない原則に反する。

  4. コードの理解が難しくなる: 継承を使用しない場合、同じ機能が複数の場所に分散して定義されるため、コードの理解が難しくなります。継承を使用することで、オブジェクトの階層構造が明確になり、コードの構造を理解しやすくなります。

使うメリットを教えて。

  1. コードの再利用性: 継承を使用することで、既存のクラスの機能を継承し、それを拡張することができます。親クラスに共通の振る舞いや属性を定義することで、子クラスはその機能を再利用することができます。これにより、コードの再利用性が向上し、冗長なコードを減らすことができます。

  2. コードの簡潔性: 継承を使用することで、新しいクラスを定義する際に親クラスの機能を利用することができます。親クラスで定義された共通の振る舞いや属性は、子クラスで再定義する必要がないため、コードがより簡潔になります。

  3. 拡張性: 継承を使用することで、既存のクラスの機能を拡張し、新しい機能を追加することができます。親クラスの振る舞いを変更する必要がある場合は、親クラスを変更するだけで、すべての子クラスに影響を与えることができます。

  4. ポリモーフィズムの実現: 継承を使用することで、ポリモーフィズムを実現することができます。異なるクラスが同じインターフェースを持つことで、同じメソッドを使用して異なるクラスのオブジェクトを操作することができます。

  5. コードの構造化: 継承を使用することで、コードの階層構造を明確にすることができます。親クラスと子クラスの関係が明確になるため、コードの理解やメンテナンスが容易になります。

ゲームの主人公が敵からダメージを受けた場合を考えた事例

# play_game.py 
class Character:
    def __init__(self, name, HP):
        self.name = name
        self.HP = HP

class Player(Character):
    def __init__(self, name, HP, level):
        super().__init__(name, HP)
        self.level = level

class Enemy(Character):
    def __init__(self, name, HP, damage):
        super().__init__(name, HP)
        self.damage = damage

if __name__ == '__main__':
    # キャラクター名:ライアン HP:100 レベル:5 として設定
    laian = Player('ライアン',100,5)
    # モンスター名:スライム HP:20 ダメージ:3 として設定
    slime = Enemy('スライム',20, 3)
    # ライアンが攻撃を受ける前のHPを表示
    print(laian.HP)
    # 主人公が敵からダメージを受けた場合(HPからダメージを引き算して残りのHPを計算する)
    laian.HP = laian.HP - slime.damage
    # ライアンが攻撃を受けた後のHPを表示
    print(laian.HP)

実行結果:

$ python play_game.py 
100  
97   

クラス記述する時に、selfをつけるのって、面倒くさくて、なんのメリットがあるの?

  1. インスタンス変数へのアクセス: インスタンスメソッド内でインスタンス変数にアクセスできます。

  2. インスタンスメソッドの呼び出し: インスタンスメソッド内で他のインスタンスメソッドを呼び出すことができます。

  3. クラス変数へのアクセス: インスタンスメソッド内でクラス変数にアクセスできます。

  4. これにより、self を使用することで、インスタンスごとに異なる状態を保持し、操作することができます。

ゲームの主人公が複数のスライムからダメージを受けた場合を考えた事例

# play_game.py 
class Character:
    def __init__(self, name, HP):
        self.name = name
        self.HP = HP

class Player(Character):
    def __init__(self, name, HP, level):
        super().__init__(name, HP)
        self.level = level

class Enemy(Character):
    def __init__(self, name, HP, damage):
        super().__init__(name, HP)
        self.damage = damage

if __name__ == '__main__':
    # キャラクター名:ライアン HP:100 レベル:5 として設定
    laian = Player('ライアン',100,5)
    # モンスター名:スライム01 HP:20 ダメージ:3 として設定
    slime_01 = Enemy('スライム01',20, 3)
    # モンスター名:スライム02 HP:18 ダメージ:2 として設定
    slime_02 = Enemy('スライム01',18, 2)
    # ライアンが攻撃を受ける前のHPを表示
    print(laian.HP)
    # 主人公がスライム01からダメージを受けた場合(HPからダメージを引き算して残りのHPを計算する)
    laian.HP = laian.HP - slime_01.damage
    print(laian.HP)
    # 主人公がスライム02からダメージを受けた場合(HPからダメージを引き算して残りのHPを計算する)
    laian.HP = laian.HP - slime_02.damage
    # ライアンが攻撃を受けた後のHPを表示
    print(laian.HP)

実行結果:

$ python play_game.py 
100  
97
95

ここまで来てもおそらくselfの意味が湧かないと思いますが、さらに説明は続きます。
ここは、クラスメソッド内に「self」をつけるとインストラクター内のすべての変数を参照できるだと覚えて。


ラッパーってなんに?(クラスがあるのになぜラッパー?)

Pythonにおいてクラスへのアクセスをラップ(包装)する方法はいくつかあります:

  1. デコレータを使用したクラスラッパー:
    既存のクラスを変更せずに機能を追加するための方法。デコレータ関数内で新しいクラスを定義し、元のクラスの代わりに返すことで、元のクラスをラップする。

  2. 継承を使用したクラスラッパー:
    新しいクラスを定義し、ラップしたいクラスを継承することで、元のクラスをラップする。

  3. コンポジションを使用したクラスラッパー:
    クラス内に他のクラスのインスタンスを持ち、その機能を使用する方法。新しいクラスを定義し、ラップしたいクラスのインスタンスを作成し、必要に応じてそのメソッドを呼び出すことで、元のクラスをラップする。

これらの方法は、既存のクラスを変更せずに機能を追加したり、特定の振る舞いをカプセル化したりするための便利な方法です。適切な方法は、使用ケースや目的に応じて異なります。

ゲームの主人公が、バイキルトが掛かっているスライムからダメージを受けた場合
ラッパーを使うことにより、一時的スライムから受けるダメージを2倍にすることができる

# play_game_02.py 
class Character:
    def __init__(self, name, HP):
        self.name = name
        self.HP = HP

class Player(Character):
    def __init__(self, name, HP, level):
        super().__init__(name, HP)
        self.level = level

class Enemy(Character):
    def __init__(self, name, HP, damage):
        super().__init__(name, HP)
        self.damage = damage

# バイキルトラッパー
def baikilled_rapper(name, HP, damage, bairitsu):
    return Enemy(name, HP, damage*bairitsu)

if __name__ == '__main__':
    # キャラクター名:ライアン HP:100 レベル:5 として設定
    laian = Player('ライアン',100,5)
    # モンスター名:スライム01 HP:20 ダメージ:3 バイキルトは2倍
    slime_baikilled = baikilled_rapper('スライム',20, 3, 2)
   
    # ライアンが攻撃を受ける前のHPを表示
    print(laian.HP)
    # 主人公がスライム01からダメージを受けた場合(HPからダメージを引き算して残りのHPを計算する)
    laian.HP = laian.HP - slime_baikilled.damage
    # ライアンが攻撃を受けた後のHPを表示
    print(laian.HP)
$ python play_game_02.py 
100
94

※これを応用すれば、痛恨の一撃なども可能となる


インスタンスってなに?(これがないとだめ?)

ドラゴンクエスト(Dragon Quest)のゲームを例に、Pythonのクラスとインスタンスを説明する
以下に勇者クラス、モンスタークラス、攻撃する/攻撃を受けるクラスを実装する

# 基底クラス
class Character:
    def __init__(self, name, HP):
        self.name = name
        self.HP = HP
        
#プレーヤークラス
class Player(Character):
    def __init__(self, name, HP, level, attack_value):
        super().__init__(name, HP)
        self.level = level
        self.attack_value = attack_value

    def show_status(self):
        print(f"名前: {self.name}, レベル: {self.level}, HP: {self.HP}")
        
# モンスタークラス
class Enemy(Character):
    def __init__(self, name, HP, attack_value):
        super().__init__(name, HP)
        self.attack_value = attack_value

    def show_status(self):
        print(f"名前: {self.name}, HP: {self.HP}")
    

# 攻撃する/攻撃を受けるクラス
class Player_Attack(Character):
    def __init__(self, name, HP, val):
        super().__init__(name, HP)
        self.val = val

    def attack(self, enemy):
        self.enemy = enemy
        print(f"{self.name}の攻撃!")
        self.take_damage(self.val)

    def take_damage(self, damage):
        print(f"{self.enemy.name}{damage}のダメージを受けた!")
        self.enemy.HP -= damage
        if self.enemy.HP <= 0:
            print(f"{self.enemy.name}を倒しました!")

戦闘シーン1:勇者がスライムを攻撃する

if __name__ == '__main__':
    # 戦闘シーンその1
    print('戦闘シーン1')
    # 勇者のインスタンスの作成
    hero = Player("勇者", 100, 1, 10)
    # スライムのインスタンスの作成
    slime = Enemy("スライム", 10, 3)
    print(f'{slime.name}が現れました!')
    slime.show_status()
    # 勇者が攻撃するインスタンスの作成
    hero_at = Player_Attack(hero.name, hero.HP, hero.attack_value)
    # 勇者がスライムを攻撃する
    hero_at.attack(slime)
    slime.show_status()
    print('戦闘完了')

実行結果:

$ python play_game_03.py 
戦闘シーン1
スライムが現れました!
名前: スライム, HP: 10
勇者の攻撃!
スライムは10のダメージを受けた!
スライムを倒しました!
名前: スライム, HP: 0
戦闘完了

このようにして、勇者、スライム、攻撃というそれぞれのインスタンスはそれぞれ以下:

# 勇者のインスタンスの作成
hero = Player("勇者", 100, 1, 10)
# スライムのインスタンスの作成
slime = Enemy("スライム", 10, 3)
# 勇者が攻撃するインスタンスの作成
hero_at = Player_Attack(hero.name, hero.HP, hero.attack_value)

勇者がスライムを攻撃する場合は以下の3部構成で実装している
勇者の攻撃インスタンス: hero_at
攻撃メソッド: attack()
攻撃する敵もインスタンス: slime

# 勇者がスライムを攻撃する
hero_at.attack(slime)

これを駆使しながら、戦闘シーンのメッセージを表示することができる
一回目の戦闘でスライムのHPが0になったため、戦闘終了した。

戦闘シーンその2

逆に勇者がスライムへ攻撃し、そしてスライムから勇者へ反撃する戦闘シーンは以下に示す。

    # 戦闘シーンその2
    print('戦闘シーン2')
    # 勇者のインスタンスの作成
    hero = Player("勇者", 100, 1, 10)
    # スライムのインスタンスの作成
    slime = Enemy("スライム", 15, 3)
    print(f'{slime.name}が現れました!')
    slime.show_status()
    # 勇者が攻撃するインスタンスの作成
    hero_at = Player_Attack(hero.name, hero.HP, hero.attack_value)
    hero_at.attack(slime)
    slime.show_status()
    # スライムが攻撃するインスタンスの作成
    slime_at = Player_Attack(slime.name, slime.HP, slime.attack_value)
    slime_at.attack(hero)
    hero.show_status()

実行結果:

戦闘シーン2
スライムが現れました!
名前: スライム, HP: 15
勇者の攻撃!
スライムは10のダメージを受けた!
名前: スライム, HP: 5
スライムの攻撃!
勇者は3のダメージを受けた!
名前: 勇者, レベル: 1, HP: 97

このように交互の攻撃を再現することができる


クラスメソッドとクラス変数の違いってなに?

クラスにイオベホマラー魔法を実装する

class Hero:
    party_count = 0  # クラス変数

    def __init__(self, name, level, hp):
        self.name = name
        self.level = level
        self.hp = hp
        Hero.party_count += 1  # インスタンスが作成されるたびにクラス変数をインクリメント

    @classmethod
    def get_party_count(cls):  # クラスメソッド
        return cls.party_count

    # ベホマラー
    @staticmethod
    def heal_all_party_members(party, heal_val):  # スタティックメソッド
        for hero in party:
            hero.hp += heal_val  # 全てのパーティーメンバーのHPを回復

    # イオ
    @staticmethod
    def bang_attack(party, damage):
        for hero in party:
            hero.hp -= damage
    
    def show_status(self):
        print(f"名前: {self.name}, レベル: {self.level}, HP: {self.hp}")

ドラクエでパーティー全員がイオ魔法を受けた後にベホマラーで回復したシーンを考える

# インスタンスの作成
hero1 = Hero("勇者1", 1, 100)
hero2 = Hero("勇者2", 1, 120)
hero3 = Hero("勇者3", 1, 130)

# クラスメソッドの呼び出し
print("パーティーの人数:", Hero.get_party_count())  # 出力: パーティーの人数: 3

# スタティックメソッドの呼び出し
party = [hero1, hero2, hero3]

print('初期ステータス:')
for hero in party:
    hero.show_status()

print('イオの攻撃を受ける:')
# イオの攻撃を受ける
Hero.bang_attack(party, 50)

# 各勇者のステータス表示
for hero in party:
    hero.show_status()

print('ベホマラー回復:')
# 全員回復(ベホマラー)
Hero.heal_all_party_members(party, 30)

# 各勇者のステータス表示
for hero in party:
    hero.show_status()

この中で、クラスメソッド(イオベホマラーがメソッド):

show_status()
heal_all_party_members()
bang_attack()
get_party_count()

メンバー変数(勇者の名前、レベル、HP、パーティー人数):

name
level
hp
party_count

()が付いているのが、メソッドで、付いていないのが変数となる。
なお、partyはローカル変数(リストの中にインスタンスを格納している)

party

出力結果

パーティーの人数: 3
初期ステータス:
名前: 勇者1, レベル: 1, HP: 100
名前: 勇者2, レベル: 1, HP: 120
名前: 勇者3, レベル: 1, HP: 130
イオの攻撃を受ける:
名前: 勇者1, レベル: 1, HP: 50
名前: 勇者2, レベル: 1, HP: 70
名前: 勇者3, レベル: 1, HP: 80
ベホマラー回復:
名前: 勇者1, レベル: 1, HP: 80
名前: 勇者2, レベル: 1, HP: 100
名前: 勇者3, レベル: 1, HP: 110

「if _name_ == '_main_':」というおまじないについて

if __name__ == '__main__': のおまじないを使用する利点は次の通りです:

  1. 再利用性の向上: モジュールとして定義された関数やクラスは、他のスクリプトからインポートして再利用することができます。このおまじないを使用することで、モジュール内の関数やクラスの定義と、モジュールが直接実行されたときにのみ実行されるコードを分離することができます。これにより、再利用性が向上し、モジュールの利用が容易になります。

  2. テスト容易性の向上: モジュールが直接実行された場合、通常はそのモジュールに関するテストコードやデバッグ用のコードを実行します。このおまじないを使用することで、モジュールが直接実行された場合の振る舞いをテストしやすくなります。また、このおまじないを使用して、テストコードと本番用のコードを分離することもできます。

  3. 不必要な処理の回避: モジュールが直接実行された場合にのみ実行されるコードを、このおまじないを使用することで明示的に指定することができます。これにより、不必要な処理や副作用が他のスクリプトからモジュールがインポートされた際に発生するのを防ぐことができます。

総括すると、if __name__ == '__main__': のおまじないを使用することで、モジュールの再利用性を向上させ、テスト容易性を高め、不必要な処理の回避が可能になります。これは、Pythonスクリプトをモジュールとして設計する際に非常に役立ちます。


感想:

クラスについての説明は意外に難しいと思いました。
なお、実際にゲームを実装する際に、グラフィックだったり、マップだったり、戦闘の動画シーンなどがあるので、こんなに簡単と言う訳にはいかないです。

0
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?