【Python超入門】「クラスの使い方」と「オブジェクト指向」を初心者向けにRPGで解説

Pythonの「class文の使い方」と「オブジェクト指向プログラミング」について入門者向けにRPGで解説しています。

オブジェクト指向とは

オブジェクト指向とは、データ(情報)とその操作(ふるまい)をひとまとまりの「オブジェクト」として扱う考え方です。この考え方に沿ってプログラムを書くことを「オブジェクト指向プログラミング」と呼びます。

基本要素

以下は、オブジェクト指向の基本要素を整理した表です。

項目 説明 RPGの例
クラス オブジェクトの設計図。属性やメソッドを定義する。 キャラクターの設計図。「キャラクター」というクラスには、名前・レベル・HPなどの属性と、攻撃・回復などのメソッドが定義されている。
オブジェクト Pythonで扱うすべての実体(数値、文字列、リスト、関数、クラスから作られたもの全て) 「勇者ぴこり」や「魔法使いルーナ」など、ゲーム内のキャラクター。
インスタンス クラスから作られたオブジェクト。 「キャラクター」クラスから「勇者ぴこり」というインスタンス(実際のキャラクター)を作る。
属性 オブジェクトが持つデータ。 名前、レベル、HPなど。
メソッド オブジェクトができる操作(関数)。 攻撃する、防御する、逃げるなど。
カプセル化 属性を外部から直接いじれないようにする仕組み。 キャラクターのHPを勝手に変更できないようにして、メソッド経由でしか操作できないようにする。
継承 既存のクラスを元に新しいクラスを作る仕組み。 「キャラクター」クラスを元に、「村人」クラスなどを作る。
ポリモーフィズム 同じメソッド名でも、クラスごとに動作が変わること。 「attack()」メソッドが、戦士なら剣で攻撃、魔法使いなら魔法で攻撃になる。

イメージ

例えば、「キャラクター設計図」というクラスから、「勇者ぴこり」というインスタンスを生成すると、以下のようになります。

[クラス] キャラクター設計図
 ├─ 属性: 名前, レベル, HP
 └─ メソッド: 攻撃, 防御, 逃げる

↓ クラスからインスタンス(具体的なキャラクター)を生成

[インスタンス] 勇者ぴこり
 ├─ 名前: 兎野ぴこり
 ├─ レベル: 5
 ├─ HP: 120

では、具体的にどのようにPythonでクラスを定義したり、インスタンスを生成するのか次節で解説します。

「クラスの定義」と「インスタンスの生成」

Pythonでは、class文を使ってクラス(設計図)を定義し、そこからインスタンス(実体)を生成します。

RPGだと、クラスは「キャラクターの設計図」、インスタンスは「設計図を基に作成された各キャラクター(例:勇者ぴこり、魔法使いみより)」のような関係性です。

書式

# クラスの定義
class クラス名:
    # コンストラクタ(初期化メソッド)
    def __init__(self, 引数1, 引数2, ...):
        self.変数名1 = 引数1
        self.変数名2 = 引数2

    # メソッドの定義
    def メソッド名(self, 引数3, 引数4, ...):
        # メソッドの処理内容
        処理

# インスタンス(オブジェクト)の生成
インスタンス名 = クラス名(値1, 値2)

# インスタンス変数1の値を取得
変数名 = インスタンス名.変数名1

# インスタンス変数の値を書き換え
インスタンス名.変数名2 = 値
項目 内容
class クラス名: クラス定義は class キーワードで始め、クラス名の後にコロン : を付けます。クラス名は 頭文字を大文字にするのが慣習です(例:MyClass)。
def __init__(self, ...) __init__ は「コンストラクタ」と呼ばれ、インスタンスの生成時にだけ呼び出されるメソッドです。初期値の設定に使います。関数同様、「引数1=デフォルト値, 引数2=デフォルト値」といった形でデフォルト引数を設定することもできます。
self.変数 = 値 self は「そのインスタンス自身」を指します。インスタンスごとに異なる値を持たせるために使います。
def メソッド名(self, 引数3, 引数4, ...): クラス内で定義する関数(=メソッド)は、基本的に self を第1引数に含めます。このようなメソッドをインスタンスメソッドといいます。関数同様、「引数3=デフォルト値, 引数4=デフォルト値」といった形でデフォルト引数を設定することもできます。
インスタンス名 = クラス名(...) クラス名の後に () を付けて呼び出すことで、インスタンス(実体)を生成します。
インスタンス名.変数名 インスタンス変数の値にアクセスします。インスタンス変数とは、そのインスタントが持つ固有の変数のことです。

サンプルコード①

以下は、「キャラクター」クラスを作って、「勇者ぴこり」というインスタンスを生成する例です。

# 「キャラクター」クラスの定義(設計図)
class Character:
    # コンストラクタ(初期化メソッド)
    # キャラクターが誕生するときに呼ばれる特別なメソッド
    def __init__(self, name, level=1, hp=10, mp=0):
        self.name = name    # キャラクターの名前
        self.level = level  # レベル(初期値は1)
        self.hp = hp        # HP(体力)
        self.mp = mp        # MP(魔力)

    # メソッド(攻撃)
    # キャラクターが攻撃魔法を使う動作
    def attack(self):
        print(f"{self.name}は攻撃魔法を唱えた!")

    # メソッド(防御)
    # キャラクターが防御魔法を使う動作
    def guard(self):
        print(f"{self.name}は防御魔法を唱えた!")

    # メソッド(逃げる)
    # キャラクターが戦闘から離脱する動作
    def escape(self):
        print(f"{self.name}は風のように走り去った!")

# 「勇者ぴこり」のインスタンスを生成
pikori = Character("勇者ぴこり", 5, 120, 10)

# ステータス表示とメソッド呼び出し
print(f"■{pikori.name}のステータス → Lv:{pikori.level}, HP:{pikori.hp}, MP:{pikori.mp}")
pikori.attack()  # 攻撃魔法を使う
pikori.guard()   # 防御魔法を使う
pikori.escape()  # 逃げる

# 「魔法使いルーナ」のインスタンスを生成
luna = Character("魔法使いルーナ", 4, 100, 30)

# ステータス表示とメソッド呼び出し
print(f"\n■{luna.name}のステータス → Lv:{luna.level}, HP:{luna.hp}, MP:{luna.mp}")
luna.attack()  # 攻撃魔法を使う
luna.guard()   # 防御魔法を使う
luna.escape()  # 逃げる

# 「勇者ぴこり」のインスタンス変数(HP、MP)を更新
pikori.hp = 110
pikori.mp = 5

# 「勇者ぴこり」の更新後のステータスを表示
print(f"\n■{pikori.name}の更新後ステータス → Lv:{pikori.level}, HP:{pikori.hp}, MP:{pikori.mp}")
■勇者ぴこりのステータス → Lv:5, HP:120, MP:10
勇者ぴこりは攻撃魔法を唱えた!
勇者ぴこりは防御魔法を唱えた!
勇者ぴこりは風のように走り去った!

■魔法使いルーナのステータス → Lv:4, HP:100, MP:30
魔法使いルーナは攻撃魔法を唱えた!
魔法使いルーナは防御魔法を唱えた!
魔法使いルーナは風のように走り去った!

■勇者ぴこりの更新後ステータス → Lv:5, HP:110, MP:5

サンプルコード②

インスタンス変数はクラス定義の中であらかじめ用意しておくことが多いですが、インスタンス生成後に新しい変数を追加することも可能です。これは「動的な属性追加」と呼ばれ、柔軟な設計ができる反面、管理には注意が必要です。

# 「キャラクター」クラスの定義(設計図)
class Character:
    # コンストラクタ(初期化メソッド)
    # キャラクターが誕生するときに呼ばれる特別なメソッド
    def __init__(self, name, level=1, hp=10, mp=0):
        self.name = name    # キャラクターの名前
        self.level = level  # レベル(初期値は1)
        self.hp = hp        # HP(体力)
        self.mp = mp        # MP(魔力)

    # メソッド(攻撃)
    # キャラクターが攻撃魔法を使う動作
    def attack(self):
        print(f"{self.name}は攻撃魔法を唱えた!")

    # メソッド(防御)
    # キャラクターが防御魔法を使う動作
    def guard(self):
        print(f"{self.name}は防御魔法を唱えた!")

    # メソッド(逃げる)
    # キャラクターが戦闘から離脱する動作
    def escape(self):
        print(f"{self.name}は風のように走り去った!")

# 「勇者ぴこり」のインスタンス(実体)を生成
pikori = Character("勇者ぴこり", 5, 120, 10)

# 「勇者ぴこり」に新しい属性「job(職業)」を追加
pikori.job = "勇者"

# ステータス表示とメソッド呼び出し
print(f"■{pikori.name}のステータス → Lv:{pikori.level}, HP:{pikori.hp}, MP:{pikori.mp}, 職業:{pikori.job}")
■勇者ぴこりのステータス → Lv:5, HP:120, MP:10, 職業:勇者

「pikori」というインスタンス作成後、「pikori.job = “勇者”」書くことで、job という属性が そのインスタンスだけに追加されます。このように、クラス定義には存在しない属性でも、インスタンス単位で持たせることができます。ただし、この方法で追加された属性は、他のインスタンスには存在しません

サンプルコード③

例えば、サンプルコード②の後に、以下のように「魔法使いルーナ」のインスタンスを生成し、 luna.job を表示しようとすると、エラーになります。

# 「魔法使いルーナ」のインスタンスを生成
luna = Character("魔法使いルーナ", 4, 100, 30)

print("「魔法使いルーナ」の職業:{luna.job}")
-Traceback (most recent call last):
File “/tmp/main.py”, line 2, in
import user_code
File “/tmp/user_code.py”, line 38, in
print(f”「魔法使いルーナ」の職業:{luna.job}”)
^^^^^^^^
AttributeError: ‘Character’ object has no attribute ‘job’

このように、インスタンス変数は柔軟に追加できますが、クラス全体で共通の属性にしたい場合は、クラス定義内で明示的に書いておく方が安全です。

クラス変数

Pythonでは、クラス変数は「クラス全体で共通の値」を持つ変数です。 すべてのインスタンス(実体)で共有されるため、個別のキャラクターではなく「世界設定」や「共通ルール」のようなデータを扱うときに使います。

書式

# クラスの定義
class クラス名:
    変数名 = 値  # クラス変数(全インスタンスで共通)


# クラス変数へのアクセス
クラス名.クラス変数名

クラス変数は、クラス文の「直下」に記述します。__init__ の中ではなく、クラスブロックの外側です。

サンプルコード①

class Character:
    # クラス変数(全キャラ共通の設定)
    world_name = "ジャポン大陸"

    def __init__(self, name):
        self.name = name

# キャラクターを生成
pikori = Character("勇者ぴこり")
luna = Character("魔法使いルーナ")

# クラス変数の参照
print(f"{pikori.name}の世界 → {Character.world_name}")
print(f"{luna.name}の世界 → {luna.world_name}")
勇者ぴこりの世界 → ジャポン大陸
魔法使いルーナの世界 → ジャポン大陸

このように、クラス変数はインスタンス(キャラ)ごとに違う値を持たず、すべてのインスタンスで共有されます。

サンプルコード②

クラス文で定義されていなくても、あとからクラス変数を追加できます。
以下は、サンプルコード①にクラス変数を追加するコードを追加したものです。

class Character:
    # クラス変数(全キャラ共通の設定)
    world_name = "ジャポン大陸"

    def __init__(self, name):
        self.name = name

# キャラクターを生成
pikori = Character("勇者ぴこり")
luna = Character("魔法使いルーナ")

# クラス変数の追加
Character.language = "ジャポン語"

# インスタンスからも参照できる
print(f"{pikori.name}の言語 → {pikori.language}")
print(f"{luna.name}の言語 → {luna.language}")
勇者ぴこりの言語 → ジャポン語
魔法使いルーナの言語 → ジャポン語

クラス変数へ代入する際、クラス文で定義されていなかった場合、クラス内に自動的に生成されます。

クラス変数よりインスタンス変数が優先

Pythonでは、同じ名前の変数が「クラス変数」と「インスタンス変数」の両方に存在する場合、インスタンス変数が優先されるというルールがあります。
これは「個別の設定(インスタンス変数)」が「全体共通の設定(クラス変数)」よりも強い、というイメージです。

以下は、クラス変数とインスタンス変数が同名(world_name)の場合です。

class Character:
    # クラス変数(全キャラ共通の設定)
    world_name = "ジャポン大陸"

    def __init__(self, name, world_name):
        self.name = name
        self.world_name = world_name  # ← インスタンス変数が優先される

# キャラクターを生成
pikori = Character("勇者ぴこり", "アメリア大陸")

# 結果を表示
print(pikori.world_name)  # アメリア大陸
アメリア大陸

この場合、pikori インスタンスには self.world_name が設定されているため、クラス変数ではなくインスタンス変数の "アメリア大陸" が参照されます。

以下は、world_nameというインスタンス変数が存在しない場合です。

class Character:
    # クラス変数(全キャラ共通の設定)
    world_name = "ジャポン大陸"

    def __init__(self, name, world_name):
        self.name = name
        # self.world_name を定義していない

# キャラクターを生成
pikori = Character("勇者ぴこり", "アメリア大陸")

# 結果を表示
print(pikori.world_name)  # ジャポン大陸
ジャポン大陸

この場合、pikori インスタンスには world_name というインスタンス変数が存在しないため、クラス変数の "ジャポン大陸" が参照されます。

クラス変数を後から変更した場合の挙動

インスタンス変数が存在しない場合、クラス変数を参照するので、クラス変数を変更すると既存インスタンスにも影響します。一方、インスタンス変数が存在する場合、そのインスタンスは自分の変数を持っているので、クラス変数を変更しても影響を受けません。

以下は、world_nameというインスタンス変数が存在しない場合の例です。

class Character:
    world_name = "ジャポン大陸"  # クラス変数

    def __init__(self, name):
        self.name = name

# インスタンスの生成
pikori = Character("勇者ぴこり")
mimi = Character("魔法使いミミ")

print(pikori.world_name)  # ジャポン大陸
print(mimi.world_name)    # ジャポン大陸

# クラス変数を変更
Character.world_name = "アメリア大陸"

print(pikori.world_name)  # アメリア大陸 ← 影響を受ける
print(mimi.world_name)    # アメリア大陸 ← 影響を受ける
ジャポン大陸
ジャポン大陸
アメリア大陸
アメリア大陸

インスタンス変数が無い場合、クラス変数を参照するので、クラス変数を後から変更すると他のインスタンスにも影響します。
以下は、world_nameというインスタンス変数が存在している場合の例です。

class Character:
    world_name = "ジャポン大陸"  # クラス変数

    def __init__(self, name, world_name):
        self.name = name
        self.world_name = world_name  # インスタンス変数で上書き

# インスタンス生成
pikori = Character("勇者ぴこり", "アメリア大陸")
mimi = Character("魔法使いミミ", "エルドラ大陸")

print(pikori.world_name)  # アメリア大陸
print(mimi.world_name)    # エルドラ大陸

# クラス変数を変更
Character.world_name = "ノルディア大陸"

print(pikori.world_name)  # アメリア大陸 ← 影響なし
print(mimi.world_name)    # エルドラ大陸 ← 影響なし
アメリア大陸
エルドラ大陸
アメリア大陸
エルドラ大陸

インスタンス変数がある場合、インスタンス変数が優先されて参照されるため、クラス変数の変更は影響しません。

クラスメソッド

クラスメソッドは、クラス全体に関わる処理を行うメソッドです。クラス変数にアクセスしたり、クラス全体の設定を扱うときに使用し、クラス名から直接呼びだすのが特徴です。

書式

# クラスの定義
class クラス名:
    # クラスメソッド
    @classmethod
    def メソッド名(cls):

クラスメソッドは、メソッド名の上に @classmethod というデコレータをつけます。
そして、丸括弧内の引数には cls を指定します。これは「クラス自身」を指します(self はインスタンス)。

サンプルコード

以下は、クラスメソッドを定義して呼び出す例です。

class Character:
    # クラス変数(全キャラ共通)
    world_name = "ジャポン大陸"

    @classmethod
    def show_world(cls):
        print(f"この世界は 「{cls.world_name}」 です")

# クラスメソッドの呼び出し(インスタンスではなくクラスから直接呼び出し)
Character.show_world()
この世界は 「ジャポン大陸」 です

スタティックメソッド

スタティックメソッドとは、クラスに属するが、インスタンスやクラスの状態に依存しないメソッドです。「便利関数をクラスの中にまとめたいとき」 などに使われます

書式

class クラス名:
    @staticmethod
    def メソッド名(引数...):
        処理
  • @staticmethod デコレータを付け、self(インスタンス自身)や cls(クラス自身)を引数につけません。

サンプルコード

以下は、スタティックメソッドを定義して呼び出す例です。

class Character:
    # 通常攻撃のダメージ計算を行うスタティックメソッド
    @staticmethod
    def normal_attack(attack_power, enemy_defense):
        # ダメージ = 攻撃力 - 防御力(※0未満なら0とする)
        return max(0, attack_power - enemy_defense)

    # クリティカル攻撃のダメージ計算を行うスタティックメソッド
    @staticmethod
    def critical_attack(attack_power, enemy_defense):
        # 1.5倍ダメージ
        return max(0, int((attack_power * 1.5) - enemy_defense))


# 勇者ぴこりの攻撃力
pikori_attack = 40
# 敵スライムの防御力
slime_defense = 10

# 通常攻撃(インスタンス化せずに呼び出せる)
damage1 = PikoriUtil.normal_attack(pikori_attack, slime_defense)
print(f"勇者ぴこりの通常攻撃!スライムに {damage1} のダメージ!")

# クリティカル攻撃(インスタンス化せずに呼び出せる)
damage2 = PikoriUtil.critical_attack(pikori_attack, slime_defense)
print(f"勇者ぴこりの会心の一撃!スライムに {damage2} のダメージ!")
勇者ぴこりの通常攻撃!スライムに 30 のダメージ!
勇者ぴこりの会心の一撃!スライムに 50 のダメージ!

「クリティカル計算」はキャラの状態に依存せず、計算ルールそのものなのでスタティックメソッドで処理を書いています。

クラス継承

クラス継承とは、すでにあるクラスの機能を引き継いで、新しいクラスを作ることです。
継承元のクラスを「スーパークラス(親クラス)」、それを用いて新しく作成するクラスを「サブクラス(親クラス)」と言います。
共通機能はスーパークラスに集約し、追加機能だけをサブクラスにまとめることで、プログラム全体のコードの記述量を減らすことが出来ます。

RPGだと、「キャラクタークラスをベースにして、敵キャラのクラスを作る」といった感じです。

書式

class スーパークラス名():
    # 初期化メソッド
    def __init__(self, ...):
        初期化メソッドの処理
    def メソッド名(self):
        メソッドの処理

    スーパークラスの内容   

class サブクラス名(スーパークラス名):
    # 初期化メソッド
    def __init__(self, ...):
        # スーパークラスの初期化を呼び出す
        super().__init__(...)  
    # スーパークラスと同じ名前のメソッドを定義すると「オーバーライド」になる
    def メソッド名(self):
        # スーパークラスで定義したメソッドを呼び出し
        super().メソッド名()
        サブクラスで上書きする処理

    サブクラスの内容

オーバーライド(override) とは、 スーパークラス(親クラス)で定義されたメソッドを、子クラス(サブクラス)で同じ名前で再定義して、処理を上書きすることです。サブクラスのインスタンスから呼び出すと、親のメソッドではなく子のメソッドが優先されます。必要に応じて、サブクラスの中で super().メソッド名() を呼び出すことで、親クラスの処理も利用できます。

サンプルコード

以下は、スーパークラスとサブクラスの使用例です。

# スーパークラス(キャラクターの基本設計)
class Character:
    # キャラクターの初期化(名前・HP・攻撃力を設定)
    def __init__(self, name, hp, attack_power):
        self.name = name              # キャラクターの名前
        self.hp = hp                  # キャラクターの体力(HP)
        self.attack_power = attack_power  # キャラクターの攻撃力

    # 現在のHPを表示するメソッド
    def show_status(self):
        return f"{self.name}のHPは {self.hp} です"

    # 攻撃メソッド(相手のHPを減らす)
    def attack(self, target):
        target.hp -= self.attack_power  # 相手のHPから攻撃力分を減らす
        print(f"{self.name}の攻撃!{target.name}に {self.attack_power} のダメージ!")

    # 倒されたときのメッセージ
    def defeat(self):
        return f"{self.name}は倒れてしまった…"

# サブクラス(敵キャラ)
class Enemy(Character):
    # 敵キャラの初期化(親クラス+経験値・ドロップアイテム)
    def __init__(self, name, hp, attack_power, exp, drop_item):
        super().__init__(name, hp, attack_power)  # 親クラスの初期化を呼び出す
        self.exp = exp                # 倒されたときに得られる経験値
        self.drop_item = drop_item    # 倒されたときに手に入るアイテム

    # 敵キャラの攻撃(演出を変更)
    def attack(self, target):
        target.hp -= self.attack_power
        print(f"{self.name}が攻撃してきた!{target.name}に {self.attack_power} のダメージ!")

    # 敵キャラが倒されたときの報酬表示
    def defeat(self):
        return f"{self.name}を倒した!経験値: {self.exp} / ドロップ: {self.drop_item}"

# 勇者(ぴこり)の生成(スーパークラスのインスタンス)
pikori = Character("勇者ぴこり", 120, 25)

# 敵キャラ(ドラゴン)の生成(サブクラスのインスタンス)
dragon = Enemy("ドラゴン", 300, 50, 500, "ドラゴンのうろこ")

# バトル開始!
turn = 1  # ターン数の初期化
while pikori.hp > 0 and dragon.hp > 0:
    print(f"\n--- ターン {turn} ---")  # ターンの区切り表示
    print(pikori.show_status())        # 勇者のHP表示
    print(dragon.show_status())        # ドラゴンのHP表示

    # 勇者の攻撃ターン
    pikori.attack(dragon)
    if dragon.hp <= 0:
        # ドラゴンのHPが0以下なら、倒された処理を表示して終了
        print(dragon.defeat())
        break

    # ドラゴンの攻撃ターン
    dragon.attack(pikori)
    if pikori.hp <= 0:
        # 勇者のHPが0以下なら、倒された処理を表示して終了
        print(pikori.defeat())
        break

    turn += 1  # 次のターンへ
— ターン 1 —
勇者ぴこりのHPは 120 です
ドラゴンのHPは 300 です
勇者ぴこりの攻撃!ドラゴンに 25 のダメージ!
ドラゴンが攻撃してきた!勇者ぴこりに 50 のダメージ!

— ターン 2 —
勇者ぴこりのHPは 70 です
ドラゴンのHPは 275 です
勇者ぴこりの攻撃!ドラゴンに 25 のダメージ!
ドラゴンが攻撃してきた!勇者ぴこりに 50 のダメージ!

— ターン 3 —
勇者ぴこりのHPは 20 です
ドラゴンのHPは 250 です
勇者ぴこりの攻撃!ドラゴンに 25 のダメージ!
ドラゴンが攻撃してきた!勇者ぴこりに 50 のダメージ!
勇者ぴこりは倒れてしまった…

コード解説

class Enemy(Character):

Enemy クラスは Character クラスを 継承 しています。これにより、Character が持つ 属性(name, hp, attack_power)メソッド(show_status, attack, defeat) をそのまま利用できます。

def __init__(self, name, hp, attack_power, exp, drop_item):
    super().__init__(name, hp, attack_power)  # 親クラスの初期化を呼び出す
    self.exp = exp
    self.drop_item = drop_item

Enemy のコンストラクタでは、まず super().__init__(...) を呼び出して、親クラス Character の初期化処理を呼び出して実行しています。これにより、name, hp, attack_power の設定は親に任せ、子クラスでは expdrop_item といった 敵キャラ固有の属性を追加しています。

def attack(self, target):
    target.hp -= self.attack_power
    print(f"{self.name}が攻撃してきた!{target.name}に {self.attack_power} のダメージ!")

スーパークラス Character にも attack() メソッドがありますが、Enemy では 同じ名前で再定義しています。これがオーバーライドです。実行時には、Enemy インスタンスの attack() が優先され、敵キャラ専用の演出が表示されます。

def defeat(self):
    return f"{self.name}を倒した!経験値: {self.exp} / ドロップ: {self.drop_item}"

スーパークラスの defeat() は「倒れてしまった…」という汎用メッセージでした。 Enemy クラスではこれをオーバーライドして、経験値やドロップアイテムの情報を返すように変更しています。これにより、同じ「倒された」というイベントでも、キャラクターの種類によって異なる処理を行うことができます。

コード全体の処理の流れをまとめると、以下のとおりです。

① キャラクターの準備
   ├─ 「勇者ぴこり」のインスタンスを生成(名前・HP・攻撃力)
   └─ 「敵ドラゴン」のインスタンスを生成(名前・HP・攻撃力・経験値・ドロップ)

② ターン開始(turn = 1)

③ バトルループ(どちらかのHPが0になるまで繰り返す)

   ┌────────────────────────────┐
   │  1. ターン数を表示する         │
   │  2. 勇者と敵のHPを表示         │
   │  3. 勇者の攻撃                 │
   │     └─ 敵のHPを減らす          │
   │     └─ 敵のHPが0以下なら終了   │
   │         └─ 敵の defeat() を表示│
   │  4. 敵の攻撃                   │
   │     └─ 勇者のHPを減らす        │
   │     └─ 勇者のHPが0以下なら終了 │
   │         └─ 勇者の defeat() を表示│
   │  5. ターン数を+1               │
   └────────────────────────────┘

④ バトル終了(どちらかが倒れたらループ終了)

⑤ 結果表示(倒された側の defeat() メッセージ)
【Python】クラス継承 (スーパークラス・サブクラス)
この記事では、Python言語でクラス継承する方法とソースコードを解説します。

デストラクタ

デストラクタとは、インスタンスが破棄されるときに自動的に呼び出されるメソッドです。
Pythonでは __del__ という特別な名前のメソッドを定義することで利用できます。

書式

class クラス名:
    def __del__(self):
        処理

__del__ はインスタンスが削除されるときに呼ばれます。明示的に del インスタンス としたときや、プログラム終了時に参照がなくなったときに実行されます。

サンプルコード

以下は、デストラクタを定義して呼び出す例です。

class Character:
    def __init__(self, name):
        self.name = name
        print(f"{self.name}が誕生した!")

    def __del__(self):
        print(f"{self.name}は倒れてしまった…")

# 勇者ぴこりを生成
pikori = Character("勇者ぴこり")

# インスタンスを削除
del pikori
勇者ぴこりが誕生した!
勇者ぴこりは倒れてしまった…

__init__ が「コンストラクタ(生成時に呼ばれる)」なのに対し、 __del__ は「デストラクタ(破棄時に呼ばれる)」です。主に「終了処理」や「リソース解放(ファイルを閉じる、接続を切るなど)」に使われます。ただし、Pythonではガベージコレクションのタイミングによって呼ばれる時期が前後することがあるため、確実な終了処理は with 構文や明示的なクローズ処理を使う方が安全です。

特殊メソッド

Python には __〇〇__ という名前で定義される「特殊メソッド(マジックメソッドとも呼ばれる)」が多数あります。これらは 特定のタイミングや演算子の利用時に自動的に呼び出される仕組みを持っています。

例:`__init__`(コンストラクタ)、`__str__`(文字列化)、`__add__`(+ 演算子の挙動)、`__len__`(len() の挙動)など

代表的な特殊メソッドをまとめると以下表のとおりです。

分類 メソッド 呼ばれるタイミング
生成・破棄 __init__(self, ...) インスタンス生成時(コンストラクタ) pikori = Character("勇者ぴこり")
__del__(self) インスタンス破棄時(デストラクタ) del pikori
文字列表現 __str__(self) print(obj)str(obj) ユーザー向けの見やすい表示
__repr__(self) repr(obj) やシェルでの評価時 開発者向けの正確な表示
演算子オーバーロード __add__(self, other) obj1 + obj2 足し算の挙動を定義
__sub__(self, other) obj1 - obj2 引き算
__mul__(self, other) obj1 * obj2 掛け算
__truediv__(self, other) obj1 / obj2 割り算
比較演算 __eq__(self, other) obj1 == obj2 等しいかどうか
__lt__(self, other) obj1 < obj2 小なり
__gt__(self, other) obj1 > obj2 大なり
コンテナ系 __len__(self) len(obj) 要素数を返す
__getitem__(self, key) obj[key] インデックス/キーアクセス
__setitem__(self, key, value) obj[key] = value 要素代入
__delitem__(self, key) del obj[key] 要素削除
__contains__(self, item) item in obj 含まれるかどうか
イテレーション __iter__(self) for x in obj イテレータを返す
__next__(self) next(obj) 次の要素を返す
呼び出し可能 __call__(self, ...) obj(...) インスタンスを関数のように呼べる
コンテキスト管理 __enter__(self) / __exit__(self, ...) with 構文 リソースの開始と終了処理

__str__(self)

Pythonでは、インスタンス(オブジェクト)を print() で表示したときに、自動的に文字列として変換される仕組みがあります。このときに使われるのが、特別なメソッド __str__() です。

以下のとき、__str__() が呼び出されて、インスタンスの中身をわかりやすい文字列になります。

  • print(インスタンス) と書いたとき
  • str(インスタンス) と明示的に変換したとき

以下は、__str__() の使用例です。

class Character:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return f"キャラクター名: {self.name}"

hero = Character("ぴこり")
print(hero)  # キャラクター名: ぴこり
キャラクター名: ぴこり

カプセル化

オブジェクト指向の重要な考え方のひとつに カプセル化 があります。これは「オブジェクトの内部データを外部から直接いじらせず、安全に操作させる仕組み」です。 PythonではJavaやC++のような厳密なアクセス制御はありませんが、慣習的に ___ を変数名の前につけることで「外から直接触らないでね」という表現になります。

サンプルコード

以下は、変数名に__ ををつけた例です。

class Character:
    def __init__(self, name, hp):
        self.name = name
        self.__hp = hp   # 外部から直接アクセスしにくい変数

    def damage(self, amount):
        self.__hp -= amount
        if self.__hp < 0:
            self.__hp = 0

    def get_hp(self):
        return self.__hp

hero = Character("勇者ぴこり", 100)
hero.damage(30)
print(hero.get_hp())  # 70
print(hero.__hp)      # AttributeError: 外からは直接アクセスできない

キャラクターのHPは直接書き換えず、damage()やget_hp()といったメソッドを通じて変化させるように取り扱うことを表します。

「変数」と「オブジェクト」

Pythonでは、序盤で学習した数値型、文字列型、リスト型、関数、さらにはクラス型そのものまで、すべてオブジェクトとして扱われます。

サンプルコード

hp = 100
print(type(hp))   # <class 'int'>

name = "兎野ぴこり"
print(type(name))   # <class 'str'>

def greet():
    print("こんぴこー")

print(type(greet))  # <class 'function'>

上記コードの変数について、

  • hp は「intクラスのインスタンス(整数オブジェクト)」
  • name は「strクラスのインスタンス(文字列オブジェクト)」
  • greet は「functionクラスのインスタンス(関数オブジェクト)」

となります。

「変数」と「オブジェクト」の関係

Pythonでは、変数はオブジェクトそのものではなく、オブジェクトへの参照 です。
例えば、 hp = 100 もしくは hp = int(100) の場合、100intクラスのインスタンス(整数オブジェクト)hp は「そのオブジェクト(インスタンス)を指す変数(参照)」となります。

サンプルコード①

以下は、 id() 関数でオブジェクトの参照を確認する例です。

hp = 100
mp = hp   # hpが参照しているオブジェクトをmpも参照する

print(hp)        # 100
print(mp)        # 100
print(id(hp))    # 両方とも同じID(オブジェクトの実体は1つ)
print(id(mp))    # hpと同じIDになる
100
100
140709548593104
140709548593104

Pythonの id(obj) は、オブジェクトのデータが格納されているメモリ上のアドレスの識別番号(ID)を返します。

変数 hpmp は、 IDが同じですが、これは同じ整数オブジェクト(100) を参照しているためです。つまり、変数は「オブジェクトそのもの」ではなく、あくまで「オブジェクトへの参照」です。

サンプルコード②

自分でクラスを定義したときも、以下のとおり同様です。

# クラスの定義
class MyClass():
    pass

# インスタンスの作成
my = MyClass()

# my2 も myと同じオブジェクトを参照
my2 = my

# 結果を表示
print(f"myのIDは{id(my)}") 
print(f"my2のIDは{id(my2)}")
print(f"myの型は{type(my)}")
myのIDは140116063273808
my2のIDは140116063273808
myの型は

変数 my と変数 my2 は両方とも同じID(オブジェクトの実体は1つ)です。つまり、 変数 mymy2オブジェクトへの参照 です。

ネット上のソースコードでは、単に「変数オブジェクトの生成」や「オブジェクトの生成」とコメントで書かれていることが多いですが、実態としては「変数はオブジェクトへの参照」であることをしっかり理解しておきましょう。

練習問題

本ページで学んだ内容を定着させるとともに、「Python 3 エンジニア認定基礎試験」や「基本情報技術者試験」の対策にも役立つ練習問題を用意しましたので、チャレンジしてみてください。

【問題1】class文の基本構文(難易度★☆☆☆☆)

Pythonでクラスを定義する正しい方法を選びなさい。

A. class Character()
B. class Character:
C. class = Character:
D. def class Character:


正解:B
解説:クラス定義はclass クラス名:で始めます。クラス名は慣習的に先頭大文字のスタイル(PascalCase)を用います。

【問題2】コンストラクタの役割(難易度★★☆☆☆)

インスタンス生成時に自動で呼び出される「コンストラクタ」として正しいものを選びなさい。

A. def __init__(self, ...)
B. def __start__(self, ...)
C. def init(self, ...)
D. def __new__(self, ...)


正解:A
解説:__init__はインスタンス生成直後に自動で呼び出され、初期化(属性のセットなど)を行う特別なメソッドです。

【問題3】selfの意味(難易度★★☆☆☆)

メソッド定義の第1引数に置くselfの説明として正しいものを選びなさい。

A. クラス全体を指すキーワード
B. そのメソッドを呼び出した「インスタンス自身」を指す参照
C. グローバル変数への別名
D. Pythonの予約語ではないため自由に省略可能


正解:B
解説:selfは「そのメソッドを呼び出したインスタンス自身」を指し、インスタンス変数や他メソッドへアクセスするために使います。

【問題4】インスタンス生成と属性アクセス(難易度★★☆☆☆)

次のうち、クラスCharacterからインスタンスを生成し、名前属性にアクセスする正しいコードはどれか。

A.

pikori = Character["勇者ぴこり"]
print(pikori.name)

B.

pikori = Character("勇者ぴこり")
print(pikori.name) 

C.

pikori = new Character("勇者ぴこり")
print(pikori.name) 

D.

pikori = Character<"勇者ぴこり">
print(name.pikori)

正解:B
解説:インスタンス生成はクラス名()で行い、属性アクセスはインスタンス.属性名の形です。

【問題5】インスタンス変数とクラス変数(難易度★★★☆☆)

クラスCharacterにおいて、全インスタンスで共有される「世界名」を定義したい。最も適切な宣言はどれか。

A.

class Character: 
    def __init__(self): 
        self.world_name = "ジャポン大陸" 

B.

class Character:
    world_name = "ジャポン大陸" 

C.

class Character(self):
     self.world_name = "ジャポン大陸" 

D.

class Character:
    def world_name(): 
        return "ジャポン大陸"

正解:B
解説:クラス変数はclassブロック直下に記述し、全インスタンスで共有されます。__init__内に書くとインスタンスごとの別属性になります。

【問題6】クラス変数の動的追加(難易度★★★☆☆)

定義後にクラスCharacterにクラス変数「language」を追加し、すべてのインスタンスから参照できるのはどれか。

A.

class Character:
    def __init__(self, name):
        self.name = name

pikori = Character("勇者ぴこり")
pikori.language = "ジャポン語"
print(pikori.language)   # → "ジャポン語"

B.

class Character:
    def __init__(self, name):
        self.name = name

pikori = Character("勇者ぴこり")
Character.language = "ジャポン語"
print(pikori.language)   # → "ジャポン語"

C.

class Character:
    def __init__(self, name):
        self.name = name

pikori = Character("勇者ぴこり")
language = "ジャポン語"
print(pikori.language)

D.

class Character:
    def __init__(self, name):
        self.name = name
    def language(self):
        return "ジャポン語"

pikori = Character("勇者ぴこり")
print(pikori.language())   # → "ジャポン語"

正解:B
解説:Character.language = ...のようにクラス属性を追加すると、インスタンスからも共有属性として参照できます。インスタンス個別の追加は共有にはなりません。

【問題7】クラスメソッドの定義と呼び出し(難易度★★★☆☆)

クラス全体に関わる設定を表示する「クラスメソッド」を正しく定義・呼び出しているものを選びなさい。

A.

# クラスの定義
class クラス名:
    def show_world(self):

# クラスメソッドの呼び出し
pikori.show_world()

B.

# クラスの定義
class クラス名:
    @classmethod
    def show_world(cls): 

# クラスメソッドの呼び出し
Character.show_world()

C.

# クラスの定義
class クラス名:
    @classmethod
    def show_world(self): 

# クラスメソッドの呼び出し
pikori.show_world()

D.

# クラスの定義
class クラス名:
    def @classmethod 
    show_world(cls):


# クラスメソッドの呼び出し
Character.show_world()

正解:B
解説:クラスメソッドは@classmethodを付け、第一引数にcls(クラス自身)を受け取ります。呼び出しはクラスから直接行うのが基本形です。

【問題8】文字列表現(難易度★★★☆☆)

print(インスタンス名)で「キャラクター名: ぴこり」と表示したい。この場合、以下のうち最も適切なクラス定義はどれか。

A.

class クラス名:
    def __repr__(self): 
         return f"キャラクター名: {self.name}"

B.

class クラス名:
    def __str__(self): 
        return f"キャラクター名: {self.name}"  

C.

class クラス名:
    def to_string(self): 
        return f"キャラクター名: {self.name}" 

D.

class クラス名:
    def __init__(self, 引数1, 引数2):
        print(self)

正解:B
解説:print(インスタンス)時の文字列化には__str__が使われます。分かりやすい表示を返すよう定義します。

【問題9】継承とsuperの使い方(難易度★★★★☆)

親クラスCharacterの初期化を子クラスEnemyで正しく呼び出すコードはどれか。

A.

class Enemy(Character):
    def __init__(self, name):
        self.__init__(name)

B.

class Enemy(Character):
    def __init__(self, name):
        Character.__init__(self, name)

C.

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

D.

class Enemy:
    def __init__(self, name):
        super.__init__(name)  

正解:C
解説:継承ではclass 子(親): ...と宣言し、親の初期化はsuper().__init__(...)で呼び出します。

【問題10】メソッドのオーバーライド(難易度★★★★☆)

子クラスEnemyattack()の演出だけを変えたい。正しい説明を選びなさい。

A. 親のattack()を削除しないと子で定義できない
B. 子クラスで同名attack()を再定義すれば、子インスタンスはその実装で動く
C. 子クラスでは親メソッド名を再利用できない
D. 変更したい場合は必ず@classmethodにする必要がある


正解:B
解説:同名メソッドを子クラスで再定義(オーバーライド)すると、子インスタンスからの呼び出しは子の実装が優先されます。演出や処理の差し替えに用いられます。

【問題11】インスタンスメソッドの呼び出し(難易度★★☆☆☆)

次のクラス定義があるとき、正しい呼び出し方を選びなさい。

class Hero:
    def attack(self):
        print("攻撃した!")

A. Hero.attack()
B. Hero().attack()
C. attack.Hero()
D. Hero->attack()


正解:B
解説:インスタンスメソッドはインスタンスを生成してから呼び出します。Hero().attack()が正しい形です。

【問題12】デフォルト引数(難易度★★☆☆☆)

次のコンストラクタで、HPを省略した場合の初期値は何か。

class Hero:
    def __init__(self, name, hp=100):
        self.name = name
        self.hp = hp

A. 0
B. None
C. 100
D. エラーになる


正解:C
解説:引数にデフォルト値を設定すると、省略時にその値が使われます。この場合はhp=100です。

【問題13】属性の動的追加(難易度★★★☆☆)

次のコードの出力はどうなるか。

hero = Hero("ぴこり")
hero.weapon = "剣"
print(hero.weapon)

A. エラーになる
B. None
C. “剣”
D. “ぴこり”


正解:C
解説:Pythonではインスタンスに後から属性を追加できます。この場合weapon属性が追加され、”剣”が出力されます。

【問題14】クラス変数とインスタンス変数の優先順位(難易度★★★☆☆)

以下のコードを実行したときの出力はどれか?

class Hero:
    job = "勇者"
    def __init__(self, job):
        self.job = job

h = Hero("魔法使い")
print(h.job)

A. 勇者
B. 魔法使い
C. None
D. エラー


正解:B
解説:インスタンス変数はクラス変数より優先されます。したがって「魔法使い」が出力されます。
補足:もし、以下のように、インスタンス変数がなければクラス変数の値である「勇者」が出力されます。

class Hero:
    job = "勇者"
    def __init__(self, job):
        pass

h = Hero("魔法使い")
print(h.job)

【問題15】スタティックメソッド(難易度★★★☆☆)

次のうち、スタティックメソッドの定義として正しいものを選びなさい。

A.

class MyClass():
    @staticmethod 
    def calc():
        return 1+1  

B.

class MyClass():
    def calc(self): 
        return 1+1 

C.

class MyClass():
    @classmethod
    def calc(cls): 
        return 1+1

D.

class MyClass():
    def staticmethod calc(): 
        return 1+1

正解:A
解説:@staticmethodを付けると、インスタンスやクラスに依存しない関数をクラス内に定義できます。

【問題16】特殊メソッドlen(難易度★★★☆☆)

次のクラスをprintしたときの出力は?

class Party:
    def __init__(self, members):
        self.members = members
    def __len__(self):
        return len(self.members)

p = Party(["勇者", "魔法使い", "僧侶"])
print(len(p))

A. 0
B. 3
C. “勇者魔法使い僧侶”
D. エラー


正解:B
解説:__len__を定義するとlen()で呼ばれます。この場合はメンバー数3が返ります。

【問題17】isinstanceの利用(難易度★★☆☆☆)

次のコードの出力は?

class Hero: pass
h = Hero()
print(isinstance(h, Hero))

A. True
B. False
C. None
D. エラー


正解:A
解説:isinstance(オブジェクト, クラス)は、そのオブジェクトが指定クラスのインスタンスかどうかを判定します。

【問題18】多重継承(難易度★★★★☆)

次のコードの出力は?

class A:
    def hello(self): print("A")

class B:
    def hello(self): print("B")

class C(A, B):
    pass

c = C()
c.hello()

A. A
B. B
C. エラー
D. None


正解:A
解説:多重継承では、クラス定義の左側(A)が優先されます。したがって”A”が出力されます。

【問題19】プロパティ(難易度★★★★☆)

次のコードの出力は?

class Hero:
    def __init__(self, hp):
        self._hp = hp
    @property
    def hp(self):
        return self._hp

h = Hero(120)
print(h.hp)

A. 0
B. 120
C. None
D. エラー


正解:B
解説:@propertyを付けると、メソッドを属性のように呼び出せます。この場合120が出力されます。

【問題20】デストラクタ(難易度★★★★☆)

次のコードの出力は?

class Hero:
    def __del__(self):
        print("さようなら")

h = Hero()
del h

A. 何も表示されない
B. “さようなら”
C. None
D. エラー


正解:B
解説:__del__はデストラクタと呼ばれ、オブジェクトが破棄されるときに呼び出されます。del hで参照が削除されると、ガベージコレクションのタイミングで「さようなら」と表示されます。ただし、実行環境によっては即時に呼ばれない場合もある点に注意が必要です。

課題(実践力強化)

Pythonプログラミングの実践力を強化したい人向けの課題を用意しました。仕様通りに動くプログラムを書いたりエラーメッセージを読んでコードを修正するという作業は、プログラミングおいて重要ですので、是非チャレンジしてみてください。

課題① クラス・クラス変数・クラスメソッドの統合設計(難易度★★★★☆)

以下の仕様を満たすPythonコードを1から書いてください。

  • クラス設計: Character クラスを定義し、クラス変数 world_name を「ジャポン大陸」に設定する
  • クラスメソッド: show_world() をクラスメソッドとして定義し、「この世界は『ジャポン大陸』です」と表示する
  • インスタンス: pikori(勇者ぴこり, Lv=5, HP=120, MP=10)と luna(魔法使いルーナ, Lv=4, HP=100, MP=30)を生成する
  • インスタンスメソッド: attack() guard() escape() をそれぞれ表示系で持たせ、pikoriluna で呼び出す
  • 出力: クラスメソッドはクラスから直接呼び出す(インスタンスからではなく)
■世界情報
この世界は「ジャポン大陸」です

■勇者ぴこりのステータス → Lv:5, HP:120, MP:10
勇者ぴこりは攻撃魔法を唱えた!
勇者ぴこりは防御魔法を唱えた!
勇者ぴこりは風のように走り去った!

■魔法使いルーナのステータス → Lv:4, HP:100, MP:30
魔法使いルーナは攻撃魔法を唱えた!
魔法使いルーナは防御魔法を唱えた!
魔法使いルーナは風のように走り去った!


【ポイント】
・クラス変数(全体共有)world_name はクラスに紐づく値で、すべてのキャラが同じ世界名を参照します。(世界設定・定数・共有のカウンタなどに使用します)
・インスタンス変数 name, level, hp, mp は init 内で self に設定され、インスタンスごとに異なる値を持ちます。(キャラの個性や状態など、各キャラ固有の情報を扱うのに使用します)
すのに向いています。
・インスタンスメソッドは、インスタンス(キャラごと)の動作に使います。self は「そのキャラ自身」を表し、個別の状態にアクセスできます。
・クラスメソッド(@classmethod と cls を受け取る): クラス全体に関わる処理(共有設定の表示・生成ロジックのバリエーションなど)に使います。cls は「Character というクラス自身」を指します。

class Character:
    # クラス変数(全キャラ共通の設定)
    # → すべてのキャラクターが同じ「世界名」を共有する
    world_name = "ジャポン大陸"

    def __init__(self, name, level=1, hp=10, mp=0):
        # インスタンス変数(キャラごとに異なる情報を保持)
        # name: キャラクターの名前
        # level: レベル(デフォルトは1)
        # hp: 体力(デフォルトは10)
        # mp: 魔力(デフォルトは0)
        self.name = name
        self.level = level
        self.hp = hp
        self.mp = mp

    # インスタンスメソッド(キャラごとに動作する)
    def attack(self):
        # 攻撃アクションを表示
        print(f"{self.name}は攻撃魔法を唱えた!")

    def guard(self):
        # 防御アクションを表示
        print(f"{self.name}は防御魔法を唱えた!")

    def escape(self):
        # 逃走アクションを表示
        print(f"{self.name}は風のように走り去った!")

    # クラスメソッド(クラス全体に関わる処理)
    @classmethod
    def show_world(cls):
        # cls はクラス自身を表す(Characterクラスそのもの)
        # クラス変数 world_name を参照して世界名を表示
        print(f"この世界は「{cls.world_name}」です")


# -------------------------------
# 動作確認
# -------------------------------

# 世界情報を表示(クラスメソッドはクラスから直接呼び出せる)
print("■世界情報")
Character.show_world()

# キャラクターを2人生成(インスタンス化)
# → __init__ が呼ばれて、それぞれのキャラの情報が設定される
pikori = Character("勇者ぴこり", 5, 120, 10)   # 名前・レベル・HP・MPを指定
luna = Character("魔法使いルーナ", 4, 100, 30) # 別のキャラを作成

# 勇者ぴこりのステータスと行動
print(f"\n■{pikori.name}のステータス → Lv:{pikori.level}, HP:{pikori.hp}, MP:{pikori.mp}")
pikori.attack()  # 勇者ぴこりが攻撃
pikori.guard()   # 勇者ぴこりが防御
pikori.escape()  # 勇者ぴこりが逃走

# 魔法使いルーナのステータスと行動
print(f"\n■{luna.name}のステータス → Lv:{luna.level}, HP:{luna.hp}, MP:{luna.mp}")
luna.attack()    # ルーナが攻撃
luna.guard()     # ルーナが防御
luna.escape()    # ルーナが逃走

課題② エラー修正(難易度★★★★☆)

以下のコードを実行するとエラーが表示されます。コードを修正して、__str__() でキャラクター名とHPが正しく表示されるようにしてください。

class Character:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return f"キャラクター名: {self.name} / HP: {self.hp}"

hero = Character("ぴこり")
print(hero)  # ここでエラー

■実行結果(エラーメッセージ: Character オブジェクトに hp がありません)

Traceback (most recent call last):
File "c:\github\sample\python\oop\str\sample_error.py", line 8, in <module>
print(hero)
File "c:\github\sample\python\oop\str\sample_error.py", line 6, in __str__
return f"キャラクター名: {self.name} / HP: {self.hp}"
^^^^^^^
AttributeError: 'Character' object has no attribute 'hp'

`AttributeError` は「その属性が存在しない」ことを示します。`__init__` で `self.hp` を初期化しておらず、`__str__()` で参照したために発生しています。インスタンス変数は `__init__` などで必ず用意しておき、表示用の `__str__()` はその前提に依存します。

■修正後のコード

class Character:
    # Character クラスの定義
    # → RPGのキャラクターを表す設計図

    def __init__(self, name, hp=100):
        # コンストラクタ(インスタンス生成時に自動で呼ばれる特別なメソッド)
        # name: キャラクターの名前
        # hp: 体力(デフォルト値は100)
        self.name = name  # インスタンス変数 name に引数を代入
        self.hp = hp      # インスタンス変数 hp に引数を代入

    def __str__(self):
        # 特殊メソッド __str__ は、print() などでインスタンスを文字列化するときに呼ばれる
        # ここではキャラクターの情報を見やすい文字列に整形して返す
        return f"キャラクター名: {self.name} / HP: {self.hp}"


# -------------------------------
# 実際の動作確認
# -------------------------------

# Character クラスから hero というインスタンスを生成
# name="ぴこり", hp=120 を指定(hp はデフォルト100だが、ここでは120に上書き)
hero = Character("ぴこり", hp=120)

# print(hero) を実行すると、自動的に __str__ メソッドが呼ばれる
# → "キャラクター名: ぴこり / HP: 120" と表示される
print(hero)

課題③ バグ修正(難易度★★★★★)

以下のコードにはバグがあります。inventory(所持品)が全インスタンスで共有されてしまい、1人がアイテム追加すると他のキャラにも反映されます。個別の所持品を持てるように修正してください。

class Character:
    # バグ:リストをクラス変数にしてしまっている
    inventory = []

    def __init__(self, name):
        self.name = name

    def add_item(self, item):
        self.inventory.append(item)

    def show_inventory(self):
        return f"{self.name}の所持品: {self.inventory}"

pikori = Character("勇者ぴこり")
luna = Character("魔法使いルーナ")

pikori.add_item("回復薬")
print(pikori.show_inventory())
print(luna.show_inventory())  # ここがおかしい(同じアイテムが見える)

■現象(共有バグ)

勇者ぴこりの所持品: [‘回復薬’]
魔法使いルーナの所持品: [‘回復薬’]

クラス変数は「クラス全体で共有」されます。可変オブジェクト(リストや辞書)をクラス変数に置くと、全インスタンスが同じコンテナを共有してしまいます。所持品のような「個別の状態」は `__init__` でインスタンス変数として新しいリストを作る必要があります。

■修正後のコード

class Character:
    def __init__(self, name):
        self.name = name
        # インスタンスごとに独立したリストを持つ
        self.inventory = []

    def add_item(self, item):
        self.inventory.append(item)

    def show_inventory(self):
        return f"{self.name}の所持品: {self.inventory}"

pikori = Character("勇者ぴこり")
luna = Character("魔法使いルーナ")

pikori.add_item("回復薬")
print(pikori.show_inventory())  # ['回復薬']
print(luna.show_inventory())    # []

課題④ エラー修正(難易度★★★★☆)

以下のコードを実行するとエラーが表示されます。インスタンスメソッドを正しく呼べるように修正してください。

class Character:
    def __init__(self, name, attack_power):
        self.name = name
        self.attack_power = attack_power

    def attack(self, target):
        print(f"{self.name}の攻撃!{target}に {self.attack_power} のダメージ!")

# バグ:インスタンスを作らずクラスから攻撃を呼ぶ
Character.attack("ドラゴン", "勇者ぴこり")

■実行結果(エラーメッセージ: 必須引数 self が足りない)

Traceback (most recent call last):
File "c:\github\sample\python\oop\method\sample_error.py", line 10, in <module>
Character.attack("ドラゴン", "勇者ぴこり")
TypeError: Character.attack() missing 1 required positional argument: 'self'

インスタンスメソッドは、第一引数に `self` を取り、インスタンスの状態にアクセスします。呼び出しは「インスタンスから」が原則で、クラスから直接呼ぶなら `@classmethod` や `@staticmethod` として定義する必要があります(今回はそうではない)。

■修正後のコード

class Character:
    def __init__(self, name, attack_power):
        self.name = name
        self.attack_power = attack_power

    def attack(self, target):
        print(f"{self.name}の攻撃!{target}に {self.attack_power} のダメージ!")

# インスタンスを生成してから呼ぶ
hero = Character("勇者ぴこり", 25)
hero.attack("ドラゴン")

課題⑤ バグ修正(難易度★★★☆☆)

以下のコードは、キャラクターの所持品を管理するクラスです。しかし、インスタンス間で所持品が共有されてしまいます。
修正して、キャラクターごとに独立した所持品リストを持てるようにしてください。

class Character:
    def __init__(self, name, inventory=[]):  # バグ:デフォルト引数にリスト
        self.name = name
        self.inventory = inventory

    def add_item(self, item):
        self.inventory.append(item)

    def show_inventory(self):
        return f"{self.name}の所持品: {self.inventory}"

pikori = Character("勇者ぴこり")
luna = Character("魔法使いルーナ")

pikori.add_item("回復薬")
print(pikori.show_inventory())
print(luna.show_inventory())  # ここがおかしい(同じアイテムが見える)

■現象(共有バグ)

勇者ぴこりの所持品: [‘回復薬’]
魔法使いルーナの所持品: [‘回復薬’]

Python では **デフォルト引数は関数定義時に一度だけ評価される** ため、リストや辞書のような可変オブジェクトを置くと、全インスタンスで共有されてしまいます。

■修正後のコード

class Character:
    def __init__(self, name, inventory=None):
        self.name = name
        # None を判定して新しいリストを作る
        self.inventory = [] if inventory is None else inventory

    def add_item(self, item):
        self.inventory.append(item)

    def show_inventory(self):
        return f"{self.name}の所持品: {self.inventory}"

pikori = Character("勇者ぴこり")
luna = Character("魔法使いルーナ")

pikori.add_item("回復薬")
print(pikori.show_inventory())  # ['回復薬']
print(luna.show_inventory())    # []

■ポイント
– デフォルト引数に可変オブジェクトを使うのは危険
– `None` をデフォルトにして、必要なら新しいリストを生成するのが定石

関連ページ(もっと学びたい人へ)

Pythonの基礎から応用例まで、以下ページから学ぶことができます。

【Python超入門】基礎から応用例まで幅広く解説
Pythonの使い方について、基礎文法から応用例まで入門者向けに解説します。
この記事を書いた人
西住技研

プログラミング言語「Python」を研究、仕事、趣味でデータ分析や作業自動化などに活用してきたノウハウを情報発信しています。
筆者の詳しいプロフィールやお問合せはこちらのページまで。
YoutubeX(旧Twitter)でも情報発信中です!

西住技研をフォローする