JEPA4Japan · チュートリアル

反復処理と内包表記

1,634文字 5分で読めます #Python

enumerate、zip、sorted、読みやすい内包表記でコレクションを変換します。

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

ループには、一つの仕事

第7章では、リスト、辞書、集合という箱を作りました。今度はPythonが箱を一つずつ訪ねて、小さな仕事をします。

  1. 一つ取る Pythonが値を受け取る
  2. 仕事をする ループの中を実行
  3. 次へ進む 最後まで繰り返す
forループは、値を一つずつ訪ねます。

必要な値を、そのまま受け取りましょう。

topics = ["Python", "Git", "SQL"]

for topic in topics:
    print("練習", topic)

表示用の番号も必要なら、enumerate()を使います。

for number, topic in enumerate(topics, start=1):
    print(number, topic)

出力:

1 Python
2 Git
3 SQL

start=1が変えるのは、enumerate()が作る番号だけです。リストや本当のインデックスは変わりません。数値のインデックス自体が必要なときだけ、range(len(topics))を選びます。

zip()で二列を組にする

カードが二列に並んでいると想像してください。zip()は一枚目同士、二枚目同士をクリップで留めます。

  1. Python 55分
  2. Git 25分
  3. SQL 40分
zip()は、同じ位置にある値を組にします。
topics = ["Python", "Git", "SQL"]
minutes = [55, 25, 40]

print(list(zip(topics, minutes)))

出力:

[('Python', 55), ('Git', 25), ('SQL', 40)]

大切な端があります。通常のzip()は、短い方が終わると止まります。

print(list(zip(["Python", "Git", "SQL"], [55, 25])))

出力:

[('Python', 55), ('Git', 25)]

SQLは何も言わず消えました。これが壊れたデータを意味するなら、zip(topics, minutes, strict=True)を使います。Python 3.10以降では、長さが違うとValueErrorになります。そのエラーの扱い方は第10章で学びます。

棚を崩さず並べ替える

sorted()は新しいリストを作ります。元のリストは、そのままです。

minutes = [55, 25, 40]
ranked = sorted(minutes, reverse=True)

print(ranked)
print(minutes)

出力:

[55, 40, 25]
[55, 25, 40]

レコードの一部分で比べたいときは、小さなkey関数を作ります。

def minutes_in(record):
    return record["minutes"]


records = [
    {"topic": "Python", "minutes": 40},
    {"topic": "Git", "minutes": 25},
    {"topic": "SQL", "minutes": 40},
]

print([record["topic"] for record in sorted(records, key=minutes_in, reverse=True)])

出力:

['Python', 'SQL', 'Git']

Pythonのソートは安定しています。PythonとSQLは同点なので、元の順番を保ちます。keyにはkey=minutes_inのように、()を付けず関数そのものを渡します。

短い注意:minutes.sort()はそのリストを変更し、Noneを返します。元の棚を残して新しい結果が欲しいなら、sorted(minutes)を使いましょう。

内包表記は、小さなコレクション工場

  1. 元のコレクション 20、35、50
  2. 必要なら絞る 30以上を残す
  3. 新しいコレクション 35、50
内包表記は、一回の分かりやすい流れで新しいコレクションを作ります。
minutes = [20, 35, 50]

long_sessions = [value for value in minutes if value >= 30]
topic_names = {name.strip().casefold() for name in [" Python ", "python", "Git"]}
lookup = {topic: value for topic, value in zip(["Python", "Git"], [20, 35])}

print(long_sessions)
print(sorted(topic_names))
print(lookup)

出力:

[35, 50]
['git', 'python']
{'Python': 20, 'Git': 35}

角括弧ならリスト、式一つの波括弧なら集合、key: valueなら辞書です。読みやすい内包表記には、はっきりした変換が一つ、多くても単純な絞り込みが一つあります。判断や表示や更新がいくつも必要なら、普通のループへ広げます。短さより、読みやすさを選びましょう。

学習パレードを作る

次の完全なプログラムをstudy_parade.pyとして保存します。データを厳密に組にし、新しいリストを並べ替え、番号を付け、元データが変わらないことも確認します。

def make_records(topics, minutes):
    return [
        {"topic": topic, "minutes": duration}
        for topic, duration in zip(topics, minutes, strict=True)
    ]


def minutes_in(record):
    return record["minutes"]


def build_report(topics, minutes, target):
    ranked = sorted(make_records(topics, minutes), key=minutes_in, reverse=True)
    lines = []

    for number, record in enumerate(ranked, start=1):
        if record["minutes"] >= target:
            status = "目標達成"
        else:
            status = "もう少し"
        lines.append(
            f"{number}. {record['topic']}:"
            f"{record['minutes']}分({status})"
        )

    return lines


topics = ["Python", "Git", "SQL", "テスト"]
minutes = [55, 25, 40, 25]
topics_before = topics.copy()

report = build_report(topics, minutes, target=30)

assert topics == topics_before
assert report[0].startswith("1. Python")

print("学習パレード")
for line in report:
    print(line)
print("元データ:", topics)

python3 study_parade.pyで実行します。

学習パレード
1. Python:55分(目標達成)
2. SQL:40分(目標達成)
3. Git:25分(もう少し)
4. テスト:25分(もう少し)
元データ: ['Python', 'Git', 'SQL', 'テスト']

Gitとテストは同点なので、安定ソートでGitが先のままです。AssertionErrorが出たら、約束した事実のどれかが崩れています。

3つの小さなチャレンジ

  1. 四つのおやつを、enumerate(start=1)で番号付き表示にする。
  2. 三つの名前と二つの得点を通常のzip()に渡し、次にstrict=Trueを追加してエラーの型を確認する。
  3. プロジェクトへ60分の記録を足し、目標を達成したテーマだけを内包表記で集める。

第9章へ進めるかな?

  • 値を直接ループできる
  • enumerate()で人向けの番号を付けられる
  • 通常のzip()が項目を落とす理由を説明できる
  • 元データを変えずにsorted()を使える
  • 内包表記を単純に保ち、混み合ったらループを選べる
  • 学習パレードを実行し、すべてのassertを通せた

次章では、これらの仕事を協力する小さな関数へ分け、それぞれの関数から名前が見える範囲を学びます。