コース進捗 コース目次 24レッスン中 24件を公開中
Pythonの基礎
データとコレクション
信頼できるプログラムを作る
オブジェクトでモデル化する
プロフェッショナルなPython
上級Python
データに規則があるなら設計図を使おう
単なるレコードなら辞書で十分なことも多くあります。1つの概念に状態、その状態を正しく保つ規則、関係する動作があるときはクラスが役立ちます。
- クラス 共有する設計図
- インスタンス 設計図から作った1つの物
- 状態 その物だけが持つ値
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つ目は同じインスタンスを明示的に渡した呼び出しです。
-
インスタンスで呼ぶ
session.label() -
selfが届く
この
session自身 -
状態を読む
self.topic - 結果を返す 表示は外側に任せる
不変条件は、利用できる全インスタンスで守る規則です。状態を公開する前に検証します。
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 = []と置くと、全インスタンスが同じリストを見つけます。クラス属性は共有定数には向きますが、各オブジェクトの個別のノートには向きません。
計算した窓を見せ、内部のノートを守ろう
プロパティは、軽い計算結果を属性の書き方で見せます。クラスメソッドは、目的の明確な別の生成経路を作ります。
- @property 読み取り用の計算した窓
- @classmethod 別の生成入口
- タプルの写真 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つの小さなチャレンジと次への準備
- 読書目標。
ReadingGoal(title, target_pages)へrecord(pages)と読み取り専用のcompletedプロパティを加えます。独立した2つの目標と、ちょうど目標に達する境界を試します。 - 共有リストの探偵。 クラス本体に
entries = []を置き、2つのインスタンスが共有することを示します。__init__のself.entriesへ移し、独立を証明します。 - 文字列の入口。
StudySession.from_text("Python:45")を加えます。解析はそこで行い、topicとminutesの全不変条件は通常のコンストラクターに守らせます。
注意点:__init__は別の値を返せず、属性の直接変更は不変条件を迂回できます。isは値ではなく同一性を調べ、内部リストをそのまま返すと別の変更経路を漏らします。
- クラスとインスタンスを区別できる
-
selfを説明し、正しい状態を初期化できる - インスタンスごとの変更可能なデータを
self上に置ける - 同一性と、同じに見える状態を区別できる
- プロパティと目的の明確なクラスメソッドを使える
- 内部リストの安全な外側のスナップショットを返せる
- 完全なモデルのassertが通り、65分と表示される
- 3つの小さなチャレンジを終えた
次章では、柔軟な「持っている」関係にはcomposition、本当に置き換え可能な「である」関係にはinheritance、共通動作にはpolymorphismを選びます。