コース進捗 コース目次 24レッスン中 24件を公開中
Pythonの基礎
データとコレクション
信頼できるプログラムを作る
オブジェクトでモデル化する
プロフェッショナルなPython
上級Python
交換できる助っ人でロボットを組み立てる
学習ロボットが得点計算機を持っていると想像してください。ロボットは計算機を**持つ(has-a)のであり、計算機の一種ではありません。この関係がコンポジション(composition)**です。
- 中心の物 学習計画
- 助っ人 得点規則
- has-a 別の助っ人へ差し替える
助っ人を__init__()で受け取り、仕事を任せます。
class PlainHeading:
def make(self, text):
return text
class Report:
def __init__(self, heading_maker):
self.heading_maker = heading_maker
def title(self, text):
return self.heading_maker.make(text)
print(Report(PlainHeading()).title("学習計画"))
学習計画
依存する相手が見え、交換もできます。協力オブジェクトを外から渡すのは、簡単な依存性注入です。テストでは小さな偽物、本番では実際のサービスを渡せます。
同じ約束を守る助っ人へ交換する
別の助っ人が、同じ仕事を違う方法で行えます。
class PlainHeading:
def make(self, text):
return text
class LoudHeading:
def make(self, text):
return text.upper() + "!"
for helper in [PlainHeading(), LoudHeading()]:
print(helper.make("Study plan"))
Study plan
STUDY PLAN!
一つの呼び出しが、互換性のある能力を持つ異なるオブジェクトで動きます。これが**多態性(polymorphism)**です。共通の基底クラスは必須ではありません。
-
契約
make(text) - 受け取る 文字列を1つ
- 返す 見出し文字列を1つ
- 意味も同じ 驚く副作用はない
make()という名前でも、Noneを返す、データを消す、文字列を返さず表示する実装は契約違反です。使う予定の実装を、同じ小さな例でテストします。
本当のis-a関係だけを継承にする
継承(inheritance)は、サブクラスが本当に基底オブジェクトの特殊な種類であり(is-a)、安全に置き換えられるという主張です。読書活動は活動の一種として表せます。
class Activity:
def __init__(self, title, minutes):
if minutes < 0:
raise ValueError("minutes must not be negative")
self.title = title
self.minutes = minutes
def summary(self):
return f"{self.title}:{self.minutes}分"
class ReadingActivity(Activity):
def __init__(self, title, minutes, pages):
super().__init__(title, minutes)
self.pages = pages
def summary(self):
return f"{self.title}:{self.pages}ページ"
item = ReadingActivity("Python文書", 25, 12)
print(item.summary())
print([cls.__name__ for cls in ReadingActivity.mro()])
Python文書:12ページ
['ReadingActivity', 'Activity', 'object']
サブクラスはsummary()をオーバーライドしています。super()は、メソッド解決順序、つまりMROに従い、現在のクラスの次から検索を続けます。特に多重継承では、単なる「親を呼ぶ」より正確な説明です。
- ReadingActivity 最初に探す
- Activity 次に基底を見る
- object 最後の基底
置換テストをします。「Activityを約束した場所へ、追加の型検査や意味の弱体化なしでReadingActivityを渡せるか」。無理なら、そのis-a関係はおそらく誤りです。
高い家系図より小さな能力を選ぶ
継承は、コードを無料で再利用する券ではありません。ほぼ全部を上書きするサブクラス、何度も現れるisinstance()分岐、選択肢の組み合わせごとに増えるLoudJsonScoredReportのような名前は警告です。
整形、保存、得点は独立した助っ人として構成すれば、それぞれ単独で変えられます。能力に基づく関数は、必要な操作だけを頼みます。
def show(result):
print(result.render())
互換性のあるrender()を持つオブジェクトなら参加できます。契約を小さくし、継承階層も浅く保ちます。多重継承のC3 MROは決定的ですが、協調の規則も増えます。本当にモデルが必要とするときだけ選びます。
ミニ制作:交換できる学習得点
study_scoring.pyを作ります。組み込み機能だけで動く完成プログラムです。得点規則とレポーターを外から注入し、計画は相手の正確なクラスで分岐しません。
class FixedScore:
def __init__(self, points):
if isinstance(points, bool) or not isinstance(points, int) or points < 0:
raise ValueError("points must be a non-negative integer")
self.points = points
def calculate(self, minutes):
return self.points
class MinuteScore:
def __init__(self, minutes_per_point):
if (
isinstance(minutes_per_point, bool)
or not isinstance(minutes_per_point, int)
or minutes_per_point <= 0
):
raise ValueError("minutes_per_point must be a positive integer")
self.minutes_per_point = minutes_per_point
def calculate(self, minutes):
return minutes // self.minutes_per_point
class TextReport:
def render(self, rows):
lines = [
f"{title}:{minutes}分 → {points}点"
for title, minutes, points in rows
]
lines.append(f"合計:{sum(points for _, _, points in rows)}点")
return "\n".join(lines)
class CompactReport:
def render(self, rows):
total = sum(points for _, _, points in rows)
return f"{len(rows)}活動 / {total}点"
class StudyPlan:
def __init__(self, score_rule, reporter):
self.score_rule = score_rule
self.reporter = reporter
def report(self, sessions):
rows = [
(title, minutes, self.score_rule.calculate(minutes))
for title, minutes in sessions
]
return self.reporter.render(rows)
def verify_rule(rule):
result = rule.calculate(20)
assert not isinstance(result, bool)
assert isinstance(result, int) and result >= 0
def verify_reporter(reporter):
assert isinstance(reporter.render([("テスト", 10, 2)]), str)
sessions = [("Python", 45), ("読書", 30)]
minute_plan = StudyPlan(MinuteScore(5), TextReport())
fixed_plan = StudyPlan(FixedScore(3), CompactReport())
print(minute_plan.report(sessions))
print(fixed_plan.report(sessions))
for rule in [MinuteScore(5), FixedScore(3)]:
verify_rule(rule)
for reporter in [TextReport(), CompactReport()]:
verify_reporter(reporter)
print("契約テスト成功。")
python3 study_scoring.pyで実行し、次を確認します。
Python:45分 → 9点
読書:30分 → 6点
合計:15点
2活動 / 6点
契約テスト成功。
境界の注意:MinuteScore(0)は拒否されます。新しい助っ人には、正しい名前のメソッドだけでなく、振る舞いの契約全体が必要です。
3つの小さなミッション
- 助っ人を交換。
calculate(minutes)を持つBonusScoreを、StudyPlanを編集せず追加します。 - is-aをテスト。
VideoActivity(Activity)を作り、Activityの全利用者が引き続き頼れることを書きます。 - 道順を読む。 サブクラスをもう1層加え、どのオーバーライドが動くか予想してから
mro()を表示します。
第15章へ進む準備
- has-aのコンポジションとis-aの継承を区別できる
- 協力オブジェクトを注入し、交換できる
- 振る舞いの契約を説明し、テストできる
- 共通基底なしでも能力に基づく多態性を使える
- 置換可能な本当のサブタイプだけに継承を使える
- オーバーライド、
super()、単純なMROを説明できる - 深い階層と正確な型による分岐を避けられる
- 2つの得点計画を実行し、「契約テスト成功。」を確認できる
次章では、データクラスで短いデータ記録を作り、自由な選択文字列を列挙型へ置き換えます。