JEPA4Japan · チュートリアル

クラスとオブジェクト

2,211文字 7分で読めます #Python

インスタンス、メソッド、明確な初期化を使って、状態と振る舞いをまとめます。

コース進捗 コース目次 24レッスン中 24件を公開中

データに規則があるなら設計図を使おう

単なるレコードなら辞書で十分なことも多くあります。1つの概念に状態、その状態を正しく保つ規則、関係する動作があるときはクラスが役立ちます。

  1. クラス 共有する設計図
  2. インスタンス 設計図から作った1つの物
  3. 状態 その物だけが持つ値
クラスは種類を説明し、各インスタンスは自分の状態を持ちます。
class StudySession:
    def __init__(self, topic, minutes):
        self.topic = topic
        self.minutes = minutes


first = StudySession("Python", 40)
second = StudySession("Git", 25)
print(first.topic)
print(second.minutes)

出力はPythonと25です。クラスを呼ぶとインスタンスが作られ、__init__が動きます。初期化は新しい物を準備する仕事なので、None以外を返してはいけません。

selfは1つの物のかばんを運ぶ

インスタンスメソッドの最初の仮引数はselfです。インスタンスを通して呼び出すと、Pythonがそのインスタンスを自動的に渡します。

class StudySession:
    def __init__(self, topic, minutes):
        self.topic = topic
        self.minutes = minutes

    def label(self):
        return f"{self.topic}:{self.minutes}分"


session = StudySession("Python", 40)
print(session.label())
print(StudySession.label(session))

どちらもPython:40分と表示します。1つ目は束縛されたメソッド、2つ目は同じインスタンスを明示的に渡した呼び出しです。

  1. インスタンスで呼ぶ session.label()
  2. selfが届く このsession自身
  3. 状態を読む self.topic
  4. 結果を返す 表示は外側に任せる
selfは、このメソッド呼び出しに関係するインスタンスを指します。

不変条件は、利用できる全インスタンスで守る規則です。状態を公開する前に検証します。

class StudySession:
    def __init__(self, topic, minutes=0):
        topic = topic.strip()
        if not topic:
            raise ValueError("topic must not be empty")
        if isinstance(minutes, bool) or not isinstance(minutes, int) or minutes < 0:
            raise ValueError("minutes must be a non-negative integer")

        self.topic = topic
        self._minutes = minutes

    def add_minutes(self, amount):
        if isinstance(amount, bool) or not isinstance(amount, int) or amount <= 0:
            raise ValueError("amount must be a positive integer")
        self._minutes += amount
        return self._minutes

状態を変えるメソッドも、先に確認してから更新します。先頭のアンダースコアは「公開インターフェースではない」という慣習であり、privacyを強制しません。

同じ姿でも別の物かもしれない

first = StudySession("Python", 30)
second = StudySession("Python", 30)
alias = first

print(first is second)
print(first is alias)
print(first == second)

独自の等価比較を持たないこの通常のクラスでは、出力はFalse、True、Falseです。isは2つの名前が同じ物を指すかを質問します。==は等価性の質問ですが、通常のユーザー定義クラスは値ベースの比較を定義するまで同一性へ戻ります。isはvalue is Noneのような同一性確認に使い、普通の値比較の代わりにはしません。

インスタンスごとの変更可能なデータはself上で作ります。

class StudyPlan:
    def __init__(self):
        self.sessions = []

クラス本体にsessions = []と置くと、全インスタンスが同じリストを見つけます。クラス属性は共有定数には向きますが、各オブジェクトの個別のノートには向きません。

計算した窓を見せ、内部のノートを守ろう

プロパティは、軽い計算結果を属性の書き方で見せます。クラスメソッドは、目的の明確な別の生成経路を作ります。

  1. @property 読み取り用の計算した窓
  2. @classmethod 別の生成入口
  3. タプルの写真 appendせずに調べる
変更可能な内部リストを漏らさず、役立つ窓と入口を用意します。
class StudyPlan:
    def __init__(self, sessions):
        self._sessions = list(sessions)

    @property
    def total_minutes(self):
        return sum(session.minutes for session in self._sessions)

    @property
    def sessions(self):
        return tuple(self._sessions)

    @classmethod
    def from_rows(cls, rows):
        return cls(StudySession(row["topic"], row["minutes"]) for row in rows)

plan.total_minutesには丸括弧がなく、毎回計算するので古くなりません。cls(...)はfrom_rows()を呼んだ実際のクラスを保ち、通常のコンストラクターが同じ不変条件を守ります。タプルは外からのappendを防ぎ、その時点の外側のリストを記録しますが、中にある変更可能なオブジェクトまで再帰的に固定しません。

学習計画モデルを作ろう

完全な標準ライブラリのプロジェクトです。study_plan.pyへ保存します。

class StudySession:
    def __init__(self, topic, minutes=0):
        topic = topic.strip()
        if not topic:
            raise ValueError("topic must not be empty")
        if isinstance(minutes, bool) or not isinstance(minutes, int) or minutes < 0:
            raise ValueError("minutes must be a non-negative integer")
        self.topic = topic
        self._minutes = minutes

    @property
    def minutes(self):
        return self._minutes

    def add_minutes(self, amount):
        if isinstance(amount, bool) or not isinstance(amount, int) or amount <= 0:
            raise ValueError("amount must be a positive integer")
        self._minutes += amount
        return self._minutes


class StudyPlan:
    def __init__(self, title):
        title = title.strip()
        if not title:
            raise ValueError("title must not be empty")
        self.title = title
        self._sessions = []

    def add_session(self, session):
        if not isinstance(session, StudySession):
            raise TypeError("session must be a StudySession")
        self._sessions.append(session)

    @property
    def sessions(self):
        return tuple(self._sessions)

    @property
    def total_minutes(self):
        return sum(session.minutes for session in self._sessions)

    @classmethod
    def from_rows(cls, title, rows):
        plan = cls(title)
        for row in rows:
            plan.add_session(StudySession(row["topic"], row["minutes"]))
        return plan

    def report(self):
        lines = [self.title, f"合計:{self.total_minutes}分"]
        lines.extend(session.topic for session in self._sessions)
        return "\n".join(lines)


def main():
    first = StudySession(" Python ", 30)
    second = StudySession("Git", 25)
    assert first.add_minutes(10) == 40
    assert second.minutes == 25

    plan = StudyPlan("8月の計画")
    plan.add_session(first)
    snapshot = plan.sessions
    plan.add_session(second)
    assert len(snapshot) == 1 and len(plan.sessions) == 2

    rows = [{"topic": "Files", "minutes": 20}]
    assert StudyPlan.from_rows("追加", rows).total_minutes == 20
    print("テストに合格しました。")
    print(plan.report())


if __name__ == "__main__":
    main()

出力:

テストに合格しました。
8月の計画
合計:65分
Python
Git

2つのセッションは独立し、生成と更新が正しい状態を守り、呼び出し側は計画の変更可能な内部リストを受け取りません。

3つの小さなチャレンジと次への準備

  1. 読書目標。 ReadingGoal(title, target_pages)へrecord(pages)と読み取り専用のcompletedプロパティを加えます。独立した2つの目標と、ちょうど目標に達する境界を試します。
  2. 共有リストの探偵。 クラス本体にentries = []を置き、2つのインスタンスが共有することを示します。__init__のself.entriesへ移し、独立を証明します。
  3. 文字列の入口。 StudySession.from_text("Python:45")を加えます。解析はそこで行い、topicとminutesの全不変条件は通常のコンストラクターに守らせます。

注意点:__init__は別の値を返せず、属性の直接変更は不変条件を迂回できます。isは値ではなく同一性を調べ、内部リストをそのまま返すと別の変更経路を漏らします。

  • クラスとインスタンスを区別できる
  • selfを説明し、正しい状態を初期化できる
  • インスタンスごとの変更可能なデータをself上に置ける
  • 同一性と、同じに見える状態を区別できる
  • プロパティと目的の明確なクラスメソッドを使える
  • 内部リストの安全な外側のスナップショットを返せる
  • 完全なモデルのassertが通り、65分と表示される
  • 3つの小さなチャレンジを終えた

次章では、柔軟な「持っている」関係にはcomposition、本当に置き換え可能な「である」関係にはinheritance、共通動作にはpolymorphismを選びます。