🎯 この章で学ぶこと

  • クラスとインスタンスの関係
  • __init__self の意味
  • メソッドによる振る舞いの定義
  • 継承の基本と、使いどころの判断

8.1 なぜクラスが必要か

ユーザーアカウントを辞書で表すと、こうなります。

account = {"username": "tanaka", "failed": 0, "locked": False}

しかし「失敗を記録する」「ロックすべきか判定する」という処理は、辞書とは別の場所にバラバラに書くことになります。データと処理が離れていると、正しい手順を踏まずにデータを直接いじるコード(=不整合やバグの温床)が生まれがちです。

クラスは、データ(属性)と処理(メソッド)をひとつの設計図にまとめる仕組みです。

8.2 クラスを定義する

class Account:
    """ログイン失敗の記録とロック判定を持つユーザーアカウント"""

    MAX_FAILED = 5              # クラス共通の定数

    def __init__(self, username):
        self.username = username    # 属性(インスタンスごとのデータ)
        self.failed = 0
        self.locked = False

    def record_failure(self):
        """ログイン失敗を1回記録し、必要ならロックする"""
        self.failed += 1
        if self.failed >= Account.MAX_FAILED:
            self.locked = True

    def reset(self):
        """成功時に失敗カウントをリセットする"""
        self.failed = 0

    def status(self):
        state = "ロック中" if self.locked else "有効"
        return f"{self.username}: 失敗{self.failed}回 ({state})"

使ってみましょう。

alice = Account("alice")     # インスタンス(実体)を作る
bob = Account("bob")

for _ in range(5):
    alice.record_failure()   # aliceにだけ失敗を記録

print(alice.status())        # alice: 失敗5回 (ロック中)
print(bob.status())          # bob: 失敗0回 (有効)

用語を整理します。

💡 実はずっとオブジェクトを使ってきた

"abc".upper()list.append() も、文字列オブジェクト・リストオブジェクトのメソッド呼び出しです。Pythonではすべての値がオブジェクトです。クラスを学ぶと「今まで使ってきたものの正体」がわかります。

8.3 カプセル化 — 正しい入口だけを使わせる

上の Account では、失敗の記録は必ず record_failure() を通ります。ロック判定のルールがこのメソッドの中に一元化されているので、「カウントは増えたのにロック判定を忘れた」という不整合が起こりません

Pythonでは、外から直接触ってほしくない属性にアンダースコア始まりの名前を付ける慣習があります。

class ApiClient:
    def __init__(self, api_key):
        self._api_key = api_key     # 「内部用」の目印

    def masked_key(self):
        return self._api_key[:4] + "****"
🛡️ セキュリティの視点 — カプセル化は防御設計

「データを変更する経路を1本に絞り、その経路で必ず検証する」──カプセル化のこの考え方は、セキュリティ設計そのものです。現実のシステムでも、DBを直接触らせずAPIを経由させる、ファイルを直接置かせずアップロード検証を通す、といった形で同じ原則が使われています。正しい入口を1つ作り、それ以外を塞ぐ。

8.4 継承 — 共通部分を親にまとめる

既存のクラスを土台に、差分だけ書いて新しいクラスを作れます。

class AdminAccount(Account):
    """管理者アカウント。より厳しいロック条件を持つ"""

    MAX_FAILED = 3     # 管理者は3回でロック(上書き)

    def record_failure(self):
        self.failed += 1
        if self.failed >= AdminAccount.MAX_FAILED:
            self.locked = True
            print(f"[重要] 管理者 {self.username} をロックしました。監査ログを確認してください")

admin = AdminAccount("admin")
for _ in range(3):
    admin.record_failure()
print(admin.status())    # admin: 失敗3回 (ロック中)

AdminAccountAccountstatus()reset() をそのまま受け継ぎつつ、必要な部分だけ上書き(オーバーライド)しています。

✅ 継承は控えめに

継承は強力ですが、乱用すると「どの動きが親でどれが子か」追いにくいコードになります。初学者の目安:まず関数と通常のクラスで書く。明らかな「AはBの一種」の関係があるときだけ継承。迷ったら使わない、で大丈夫です。

8.5 データを運ぶだけなら dataclass

「処理はほぼ無く、データをまとめて運びたいだけ」なら、標準の dataclasses が便利です。__init__ を自動生成してくれます。

from dataclasses import dataclass

@dataclass
class LogEntry:
    time: str
    result: str
    user: str
    ip: str

entry = LogEntry("09:00:15", "FAILED", "admin", "203.0.113.9")
print(entry.user)    # admin
print(entry)         # LogEntry(time='09:00:15', result='FAILED', ...)

第13章のログ解析ツールでは、この LogEntry のような形でログの1行を構造化して扱います。

まとめ

練習問題

問題 8-1

Server クラスを作ってください。属性は nameis_up(初期値 True)。メソッド down() で停止状態に、up() で稼働状態にし、status() で「web-01 は稼働中です / 停止中です」と返すようにしてください。

解答を見る
class Server:
    def __init__(self, name):
        self.name = name
        self.is_up = True

    def down(self):
        self.is_up = False

    def up(self):
        self.is_up = True

    def status(self):
        state = "稼働中" if self.is_up else "停止中"
        return f"{self.name} は{state}です"

web = Server("web-01")
web.down()
print(web.status())   # web-01 は停止中です
問題 8-2

8.2の Account クラスに、メソッド login(password, correct_password) を追加してください。仕様:①ロック中なら「ロック中です」と表示して False を返す ②パスワードが一致したら reset() して True を返す ③不一致なら record_failure() して False を返す。

解答を見る
class Account:
    MAX_FAILED = 5

    def __init__(self, username):
        self.username = username
        self.failed = 0
        self.locked = False

    def record_failure(self):
        self.failed += 1
        if self.failed >= Account.MAX_FAILED:
            self.locked = True

    def reset(self):
        self.failed = 0

    def login(self, password, correct_password):
        if self.locked:
            print("ロック中です")
            return False
        if password == correct_password:
            self.reset()
            return True
        self.record_failure()
        return False

「ロック確認 → 照合 → 成功/失敗処理」という認証の基本フローがクラスの中に一元化されました。なお、パスワードを平文で比較・保存するのは教材用の簡略化です。正しい扱い方は第12章で学びます。

問題 8-3

次の設計のうち、カプセル化の考え方に反しているのはどれでしょう。①残高の変更は deposit() / withdraw() メソッド経由のみ ②どこからでも account.balance = 9999999 と直接書き換える ③withdraw() の中で残高不足をチェックする

解答を見る

②が反しています。検証を通らない変更経路が存在すると、残高不足チェック(③)をいくら丁寧に書いても意味がなくなります。「検証をバイパスできる経路を残さない」ことは、Webアプリの認可チェック漏れなど現実の脆弱性にも直結する考え方です。