コース進捗 コース目次 24レッスン中 24件を公開中
Pythonの基礎
データとコレクション
信頼できるプログラムを作る
オブジェクトでモデル化する
プロフェッショナルなPython
上級Python
助手には、一つの仕事
第8章では学習レポートを作りました。長いレシピも動きますが、小さな助手に分けると理解もテストも簡単です。関数には、はっきりした仕事を一つと、小さな契約を与えます。何を受け取り、何を返し、何を変更するかという約束です。
def session_label(topic, minutes=30, *, excited=False):
label = f"{topic}:{minutes}分"
if excited:
return label.upper() + "!"
return label
print(session_label("Python"))
print(session_label("Git", minutes=45, excited=True))
出力:
Python:30分
GIT:45分!
topicは必須、minutesには既定値があり、*より後ろは名前を付けて渡します。意味の分からない二つ目のTrueより、excited=Trueの方が親切です。
この契約は「テーマと時間を受け取り、文字列を返し、関数の外を変えない」です。データを返す計算と、目に見える副作用を作るprint()を分けられます。
関数も値です。第8章ではminutes_inをsorted()のkeyへ渡しました。()を付けるとその場で呼び出してしまうので、関数そのものを渡すときは付けません。
変更できる既定値は、共有のリュック
Pythonが既定値を作るのは、呼び出すたびではなくdefを実行したときです。そのためリストには、前の呼び出しで入れた物が残ります。
- リュックは一つ 既定のリストを作る
- Pythonを入れる リュックに残る
- Gitを入れる Pythonもまだある
次は、わざと間違えた例です。
def collect(topic, topics=[]):
topics.append(topic)
return topics
print(collect("Python"))
print(collect("Git"))
出力:
['Python']
['Python', 'Git']
Noneを「新しいリストを作って」という合図にします。
def collect(topic, topics=None):
if topics is None:
topics = []
topics.append(topic)
return topics
これで、省略した二回の呼び出しは別々に始まります。端の注意:呼び出し側がリストを渡した場合、そのリストはまだ変更されます。変更しない契約なら、先にtopics = list(topics)でコピーします。
名前は内側から探す
Pythonが名前を見ると、LEGBの順で探します。Local、Enclosing、Global、Built-inです。
- Local 今の関数呼び出し
- Enclosing 外側の関数
- Global このファイルの一番外
- Built-in lenなどPythonの名前
course = "Python" # Global
def make_reader():
prefix = "学習" # Enclosing
def label(minutes):
suffix = "分" # Local
return f"{prefix} {course}:{minutes}{suffix}"
return label
reader = make_reader()
print(reader(40)) # printはBuilt-in
出力:
学習 Python:40分
関数内で代入すると、通常はlocalな名前になります。合計を更新するためだけにglobalを使うのは避けましょう。古い合計を受け取り、新しい合計を返す方が明確です。隠れた共有変更があると、テスト結果が呼び出し順に左右されます。
クロージャーは、小さなことを覚えられる
先ほど返したreaderは、まだprefixを覚えています。外側の名前を覚えた関数をクロージャーと呼びます。内側の関数から外側の値を入れ替えるときだけ、nonlocalを使います。
def make_counter(start=0):
total = start
def add(minutes):
nonlocal total
total += minutes
return total
return add
counter = make_counter(10)
print(counter(20))
print(counter(15))
出力:
30
45
nonlocalがなければ、total += minutesは、まだ値のない新しいlocalのtotalを使おうとします。クロージャーは小さな非公開メモには便利ですが、プログラム全体を隠す箱にはしません。
純粋な変換は、入力を使って結果を返し、外側を変えません。表示、ファイル書き込み、呼び出し側のリスト変更は副作用です。できるだけ、中央に変換、端に副作用を置きます。
小さな分析パイプラインを作る
- 整える きれいな新規レコード
- 分析する 合計して順位付け
- 文章にする レポート文字列を返す
- 表示する 目に見える副作用は一つ
次の完全なプログラムをstudy_pipeline.pyとして保存します。
def normalize_session(session):
return {
"topic": session["topic"].strip(),
"minutes": session["minutes"],
}
def minutes_by_topic(sessions):
totals = {}
for session in sessions:
topic = session["topic"]
totals[topic] = totals.get(topic, 0) + session["minutes"]
return totals
def minutes_in(item):
return item[1]
def analyze(sessions, target=60):
clean = [normalize_session(session) for session in sessions]
totals = minutes_by_topic(clean)
ranking = sorted(totals.items(), key=minutes_in, reverse=True)
total = 0
for session in clean:
total += session["minutes"]
return {"total": total, "met": total >= target, "ranking": ranking}
def format_report(analysis, *, heading="学習レポート"):
status = "達成" if analysis["met"] else "未達成"
lines = [heading, f"合計:{analysis['total']}分", f"目標:{status}"]
for number, (topic, minutes) in enumerate(analysis["ranking"], start=1):
lines.append(f"{number}. {topic}:{minutes}分")
return "\n".join(lines)
def run_pipeline(sessions, formatter=format_report, *, target=60):
return formatter(analyze(sessions, target=target))
sessions = [
{"topic": " Python ", "minutes": 45},
{"topic": "Git", "minutes": 40},
{"topic": "Python", "minutes": 30},
]
before = [session.copy() for session in sessions]
report = run_pipeline(sessions, target=100)
assert sessions == before
assert "Python:75分" in report
print("確認に合格しました。")
print(report)
python3 study_pipeline.pyで実行します。
確認に合格しました。
学習レポート
合計:115分
目標:達成
1. Python:75分
2. Git:40分
formatterは関数値を受け取ります。計算はすべてデータを返し、目に見える副作用を作るのは最後の二つのprint()だけです。
3つの小さなチャレンジ
format_duration(minutes, *, compact=False)を書き、45分または45mを返す。- 安全な
collect()をリストなしで二回呼び、結果が独立していることを確かめる。 - 100から始まるクロージャーへ20と5を足し、実行前に二つの答えを予想する。
第10章へ進めるかな?
- 関数の入力、戻り値、副作用を説明できる
- 通常はリストを既定値にしない理由が分かる
- LEGBの順で名前を探せる
- クロージャーが覚える物と、
nonlocalが変える物を説明できる - 計算と表示を分けられる
- パイプラインを実行し、確認に合格した
次章では、正確な例外と検証で不正な入力を拒否し、これらの関数の契約を守ります。