Skip to content
This page has been auto-translated and may contain errors.View in English

制御フロー

これまでに書いてきたプログラムはすべて、毎回同じように実行されます。つまり、上から下へ、一行ずつです。単純なスクリプトならそれで十分ですが、実際のプログラムは判断を下したり処理を繰り返したりする必要があります。クイズは答えが正しいかどうかをチェックする必要があります。ゲームはプレイヤーが勝つか負けるまで動き続ける必要があります。この章では、プログラムを分岐させたり繰り返したりする方法を扱います。

制御フローはプログラムの実行経路を形作ります。条件(if/elif/else)は分岐を選択します。ループ(whilefor)はブロックを繰り返します。Pythonのforループはイテレータプロトコルであり、インデックスベースのカウンタではありません。構文と背後にあるモデルの両方を理解することで、コードがより明確になります。

Pythonの制御フローのプリミティブはif/elif/elsewhileforです。forはイテラブルに対して__iter__を呼び出し、StopIterationまでnext()を呼び出します。while__bool__または__len__を介して条件オブジェクトを評価します。breakcontinueは最も内側のループに影響します。loop-elsebreakなしでループが完了した場合のみ実行されます。

比較

判断を下す前に、何かを比較する必要があります。比較演算子はTrueまたはFalseを返します。早めに正しく覚えておくべき最も重要なこと: =は値を代入し、==は2つの値が等しいかどうかをチェックします。これらを混同するのは初心者が最もよくやる間違いの1つです。

比較演算子は対応するダンダーメソッド(__eq____lt__など)を呼び出し、boolを返します。Pythonは連鎖比較をサポートしています: 0 < x < 100 < x and x < 10として評価され、ほとんどの言語のように左から右にではありません。文字列比較はUnicodeコードポイントに基づく辞書順比較です。

比較演算子はリッチ比較メソッドを呼び出します: __eq____ne____lt____le____gt____ge__。Pythonはショートサーキットする連鎖比較をサポートしています: a < b < ca < b and b < cになりますが、bは1度だけ評価されます。isはオブジェクトの同一性(id()の等価性)をチェックし、値の等価性ではありません。値の比較には==を使い、isNoneTrueFalseにのみ使ってください。

python
5 > 3     # True
5 < 3     # False
5 == 5    # True   (注意: 二重等号; =は代入、==は比較)
5 != 3    # True   ("等しくない")
5 >= 5    # True   ("以上")
5 <= 4    # False  ("以下")

===の違いは、ほぼ全員が早い段階で躓くポイントです。代入(=)は値を格納します。比較(==)は2つの値が同じかどうかをチェックします。

文字列も比較できます。Pythonはアルファベット順に比較します:

python
"apple" == "apple"   # True
"apple" < "banana"   # True  (aはbより前)
"apple" == "Apple"   # False (大文字小文字を区別)

条件の組み合わせ

andornotは比較を組み合わせます。andは両側がtrueである必要があります。orは少なくとも片側がtrueである必要があります。notは結果を反転します。これらにより、「スコアが合格点 AND ユーザーがアクティブ」のような実世界の条件を表現できます。

andorはショートサーキットします: andは最初のfalsyなオペランドで停止し、orは最初のtruthyなオペランドで停止します。これらは単にTrueまたはFalseではなく、実際のオペランドの値を返します。notはオペランドの__bool__を呼び出し、反転した結果を返します。

andは左から右に評価し、最初のfalsyなオペランドを返します。すべてがtruthyの場合は最後のオペランドを返します。orは最初のtruthyなオペランドを返し、すべてfalsyの場合は最後のものを返します。このショートサーキットの動作は保証されています: 左側が結果を決定する場合、右側は評価されません。not xTrue if not bool(x) else Falseと同等ですが、Pythonはこれを最適化します。

python
age   = 25
score = 88

age >= 18 and score >= 80    # True  (両方がtrueでなければならない)
age < 18 or score >= 80      # True  (少なくとも片方がtrueでなければならない)
not age >= 18                # False (結果を反転する)

andは両側を必要とします。orは少なくとも片側を必要とします。notは反転します。

truthyとfalsy

Pythonのすべての値には、たとえTrueFalseでなくとも、ブール値としての解釈があります。空文字列、ゼロ、空のリスト、Noneはすべて条件内でFalseのように振る舞います。それ以外はすべてTrueのように振る舞います。つまり、if results:if len(results) > 0:と書かずにリストが空でないかをチェックします。

Pythonのtruthinessのルール: falsyな値はFalse00.0""[](){}set()Noneです。それ以外はすべてtruthyです。条件はオブジェクトの__bool__を呼び出し、__bool__が定義されていない場合は__len__にフォールバックします。長さがゼロの__len__を持つオブジェクトはfalsyです。

真偽判定は__bool__を呼び出します。__bool__が定義されていない場合、Pythonは__len__にフォールバックします: __len__ == 0のオブジェクトはfalsyです。カスタムクラスはこれらのいずれかのメソッドを実装することでtruthinessを制御します。標準のfalsy値は: False00.00j""b""[](){}set()frozenset()None、および__bool__Falseを返すか__len__0を返すオブジェクトです。

python
# これらはすべて条件内でFalseのように振る舞う:
False, 0, 0.0, "", [], {}, (), None

# それ以外はすべてTrueのように振る舞う

これは、if results:が「リストが空でない場合」と言う自然な方法であり、if name:は文字列に何かしらの内容があるかをチェックすることを意味します。

if / elif / else

if文は条件がTrueの場合のみコードブロックを実行します。elifは最初の条件がfalseだった場合にチェックする条件を追加します。elseはどの条件にも一致しなかったすべてを捕捉します。Pythonは各ブロックの内側に何が属するかを定義するために中括弧ではなくインデントを使います。

if/elif/elseは条件を上から下に評価し、最初に一致するブロックを実行します。Pythonはブロックスコープを定義するためにインデント(慣例では4スペース)を使用します。一貫性のないインデントはSyntaxErrorになります。実行されるブランチは1つだけです: 条件が一致すると、その後のすべてのelifelseはスキップされます。

Pythonはインデントをブロック区切りとして使用し、パーサによって強制されます。インタプリタは各ブランチに対してSETUP_BLOCKバイトコードを生成します。正確に1つのブランチが実行されます。elifは構文糖衣です: 裸のif文の連鎖が作るネストを回避します。各条件は遅延評価され、先行するすべての条件がfalsyだった場合のみ評価されます。

python
score = 87

if score >= 90:
    print("A grade")
elif score >= 80:
    print("B grade")
elif score >= 70:
    print("C grade")
else:
    print("Below C")

ルール:

  • ifは必須で常に最初に来ます
  • elif("else if"の略)はオプションで、必要なだけ複数指定できます
  • elseはオプションで、一致しなかったすべてを処理し、最後に来ます
  • Pythonは各ブロックの内側に何が属するかをマークするためにインデント(4スペース)を使います。中括弧はありません

インデントはオプションでも装飾でもありません。Pythonは構造を定義するためにそれを使います。一貫性のないインデントは構文エラーになります。

一行の条件

シンプルなyes/no代入のために、Pythonには三項式と呼ばれるコンパクトな一行形式があります: value_if_true if condition else value_if_false。ロジックが本当にシンプルで文章のように読める場合のみ使ってください。

条件式(三項演算子)は条件に基づいて2つの値のいずれかに評価されます。これは文ではなく式なので、値が期待されるあらゆる場所に現れます: f-stringの内側、関数の引数、代入の中などです。シンプルなyes/noの場合に使い、elifを含むものには完全版を書いてください。

条件式x if condition else yは単一の式で、条件を評価し、もう片方のブランチを実行することなくxまたはyを返します。POP_JUMP_IF_FALSEバイトコードの組み合わせにマッピングされます。if/elseブロックとは違い、複数の文にまたがることはできず、elifを含むこともできません。複雑な分岐には、完全なif/elif/elseブロックの方が明確です。

python
label = "pass" if score >= 50 else "fail"

これは三項式です。文章のように読めます。ロジックが本当にシンプルな場合に使ってください。elifを含むものには、完全版を書いてください。

whileループ

whileループは条件がTrueである限りそのブロックを繰り返します。ループが何回実行されるべきか事前にわからない場合、たとえば有効な入力を待つときやジョブが成功するまで再試行するときに使ってください。

whileは各反復の前に条件を評価し、条件がtruthyの場合のみブロックを実行します。終了条件がループ内で変化する何かに依存するループに使ってください。反復回数がわかっている場合やコレクションを反復処理する場合は、通常forの方が明確です。

whileは各反復の前に条件式に対して__bool__(または__len__)を呼び出します。ボディは条件を変更できます。内部にbreakを持つwhile Trueは、終了条件をボディの途中または最後で評価する必要がある場合の慣用的な「ループ-until」パターンです。breakがない無限ループは、ハングの一般的な原因です。

python
lives = 3

while lives > 0:
    print(f"Lives remaining: {lives}")
    lives -= 1

print("Game over")

whileは、ループが何回実行されるか事前にわからない場合に最適です。わかっている場合、またはコレクションを反復処理する場合は、forの方が明確です。

breakとcontinue

breakは反復が何回残っていてもループを即座に終了します。continueは現在の反復の残りをスキップし、条件チェックに戻ります。両方とも、それが内側にある最も内側のループにのみ影響します。

breakは最も内側のループを終了し、その直後の文に制御を移します。continueは現在のループ本体の残りをスキップし、条件チェックから再開します(またはforループの次の反復へ)。両方とも、最も内側の囲んでいるループにのみ影響します。

breakBREAK_LOOPバイトコードを発行し、ループのコードブロックを終了し、関連するelse句をスキップします。continueCONTINUE_LOOP(コンテキストによってはJUMP_ABSOLUTE)を発行し、ループヘッダから再開します。両方とも最も内側の囲んでいるループにスコープされます。Pythonにはラベル付きのbreakはありません。ネストしたループから抜けるには、ブールフラグを使うか、returnを持つ関数に再構成してください。

breakはループを即座に終了します:

python
target = 5
num    = 0

while True:
    num += 1
    if num == target:
        print(f"Found {target}")
        break   # ループを停止

breakを含むwhile True:は、終了条件が複雑な場合やループ本体の最後で発生する必要がある場合の、有効でよく使われるパターンです。

continueは現在の反復の残りをスキップし、条件チェックに戻ります:

python
num = 0

while num < 10:
    num += 1
    if num % 2 == 0:
        continue    # 偶数をスキップ
    print(num)      # 奇数のみ表示: 1, 3, 5, 7, 9

forループ

forループはシーケンスを1つずつ処理します: リスト、文字列、数値の範囲などです。forの後に指定した変数は、順番に各項目を受け取ります。自分でカウンタを管理したり長さをチェックしたりする必要はありません。

forはイテラブルに対してiter()を呼び出してイテレータを取得し、StopIterationまでnext()を呼び出します。これは、forがイテレータプロトコルを実装する任意のものに対して機能することを意味します: リスト、文字列、辞書、ファイル、レンジ、カスタムオブジェクトなど。インデックス付きシーケンスに限定されません。

for target in iterableiter(iterable)を呼び出してイテレータオブジェクトを取得し、StopIterationが発生するまで繰り返しnext(iterator)を呼び出して結果をtargetにバインドします。forループは内部でStopIterationをキャッチします。__iter__(または__iter____next__の両方)を実装するオブジェクトはイテラブルです。これにはジェネレータやファイルオブジェクトのような遅延オブジェクトも含まれます。

python
players = ["はると", "ゆい", "そうた"]

for player in players:
    print(f"Hello, {player}!")

forループは文字列(文字ごとに反復)や他の任意のシーケンス型でも機能します。

range()

range()はループするための数値のシーケンスを生成します。range(5)0, 1, 2, 3, 4を返します。開始、終了、ステップサイズを制御できます。特定の回数ループを実行する必要がある場合に使ってください。

range(start, stop, step)startから(stopを含まない)stopまで、step刻みで整数を生成します。これは遅延シーケンスです: リストを作成せず、必要に応じて数値を生成します。これによりrange(10_000_000)はメモリ効率がよくなります。3つの形式すべてが逆順カウントのために負の引数を受け入れます。

rangeは関数ではなく型です。range(n)は、シーケンスを実体化することなくO(1)でメンバーシップとインデックス指定を計算するrangeオブジェクトを作成します。len()in、スライス、逆順反復をサポートします。内部的にはstart、stop、stepのみを格納します。反復のみが必要な場合はlist(range(n))よりもこちらを優先してください。

python
for i in range(5):
    print(i)    # 0, 1, 2, 3, 4

range()には3つの形式があります:

呼び出し生成されるもの
range(5)0, 1, 2, 3, 4
range(2, 6)2, 3, 4, 5
range(0, 10, 2)0, 2, 4, 6, 8 (ステップ2)
range(5, 0, -1)5, 4, 3, 2, 1 (カウントダウン)

range()はリストを作成しません。一度に1つずつ数値を生成するので、非常に大きな範囲でも効率的です。

enumerate()

enumerate()はループしながらインデックスと値の両方を提供するので、カウンタを別途追跡する必要はありません。i, playerの部分は、各反復で自動的にペアの値を受け取ります。

enumerate(iterable, start=0)は任意のイテレータをラップし、(index, value)タプルを生成します。startパラメータはカウンタをオフセットしますが、基礎となるインデックスは変更しません。カウンタ変数を管理するよりもenumerate()を優先してください。より明確でエラーが少なくなります。

enumerateは任意のイテレータを(count, value)ペアを生成するenumerateオブジェクトでラップします。start引数は初期カウンタ値を設定します。forヘッダでのアンパック(for i, v in enumerate(...))は、各生成項目がタプルであるため機能します。enumerateはステップごとにO(1)で、カウンタ以外の追加メモリ割り当てはありません。

python
players = ["はると", "ゆい", "そうた"]

for i, player in enumerate(players):
    print(f"{i + 1}. {player}")
# 1. はると
# 2. ゆい
# 3. そうた

i, player構文はアンパックと呼ばれます。Pythonは(index, value)ペアを自動的に2つの名前に分割します。

デフォルトではenumerate()は0から始まります。それを変更するには開始値を渡してください:

python
for i, player in enumerate(players, start=1):
    print(f"{i}. {player}")    # 1から始まる

ネストしたループ

ループの中に別のループを置くことができます。内側のループは、外側のループの1回の反復ごとに完全に実行されます。これは、グリッド、組み合わせ、または2レベルの構造を持つあらゆるデータを処理する方法です。

ネストしたループは外側の長さmと内側の長さnに対してO(m × n)の反復回数を持ちます。ネストしたループ内のbreakcontinueは最も内側のループにのみ影響します。複数のレベルから抜けるには、フラグ変数を使うか、関数に再構成してください。

forループ呼び出しは新しいイテレータオブジェクトを作成します。ネストしたループはイテレータを独立して構成します。breakは最も内側のループのみを終了します。Pythonにはラベル付きのbreakはありません。一般的な回避策は、フラグ変数を使うか、内側のループを関数にカプセル化してreturnを使うことです。デカルト積には、itertools.productがネストしたforループより読みやすいです。

python
rows = [1, 2, 3]
cols = ["A", "B"]

for row in rows:
    for col in cols:
        print(f"{col}{row}", end=" ")
    print()   # 各行の後に改行
# A1 B1
# A2 B2
# A3 B3

ネストしたループ内のbreakcontinueは最も内側のループにのみ影響します。

Loop-else

Pythonのループには、breakに遭遇せずにループが終了した場合のみ実行されるelse句を付けることができます。あまり一般的に使われませんが、「リストを検索して、何も見つからなかった場合にこれを行う」と書く最もクリーンな方法です。

forまたはwhileループのelseは、breakに遭遇せずにループが正常に完了した場合(イテラブルを使い果たすか条件がfalseになる)に実行されます。これは、別途見つけたフラグ変数を必要とせずに「検索して見つからなければ報告する」ための慣用的なパターンです。

ループのelseBREAK_LOOPバイトコードによって制御されます: breakが発火した場合、elseブロックのセットアップコードは入力されません。ループが完了した場合、elseブロックが実行されます。これはifの単純なelseとは意味的に異なります。主な実用的な用途はsearch-with-breakパターンです。それ以外では、めったに見られず、それに不慣れな読者を混乱させる可能性があります。

python
target = "けんた"
names  = ["はると", "ゆい", "そうた"]

for name in names:
    if name == target:
        print(f"Found {target}")
        break
else:
    print(f"{target} not in list")   # breakが発火しなかったため実行される

breakが実行されると、elseはスキップされます。ループがシーケンスを使い果たすと、elseが実行されます。これはニッチなパターンですが、フラグ変数よりも明確です。

ソート

sorted()は新しいソート済みリストを返し、元のリストは変更しません。.sort()はリストをその場でソートし、Noneを返します。key=引数を使うと、生の値以外のもので並べ替えることができます。たとえば、名前を大文字小文字を区別せずにソートしたり、プレイヤーのタプルをスコアでソートしたりできます。

sorted()は安全なデフォルトです: 元のものを変更することはありません。.sort()はその場で変更し、Noneを返します。両方とも降順のためにreverse=Trueを受け入れます。key=引数は、比較前に各要素に適用される関数を取ります。これはソート基準とデータを分離します。

両方ともTimsortを使用します: 最悪ケースO(n log n)、ほぼソートされたデータでO(n)、安定です。.sort()は意図的にNoneを返します(コマンドクエリ分離)。key=関数は比較ごとではなく要素ごとに1度呼ばれるため、高価なキー計算は繰り返されません。key=str.lowerは非束縛メソッド参照です。key=lambda p: p[1]は特定のフィールドにアクセスするためのインライン関数です。

python
scores = [87, 42, 96, 55, 71]

ranked = sorted(scores)           # [42, 55, 71, 87, 96] (新しいリスト)
scores.sort()                     # 元のリストをソート、Noneを返す
scores.sort(reverse=True)         # [96, 87, 71, 55, 42]

両方ともkey=引数を受け入れます: 比較前に各項目に適用される関数です:

python
names = ["そうた", "はると", "ゆい"]
sorted(names, key=str.lower)       # 大文字小文字を区別しないソート

players = [("はると", 87), ("ゆい", 96), ("そうた", 55)]
sorted(players, key=lambda p: p[1])   # スコアでソート

lambdaとは?

lambda p: p[1]は一行関数です。プレイヤーのタプルを受け取り、スコアを返します。lambda関数はLambda、内包表記、zipの章で扱います。

シンプルなケースには、sorted()を使ってください。その場で変更したいリストには、.sort()を使ってください。

実践

スコアをループ処理し、合計を累積し、合格点をカウントし、サマリーを出力します:

python
raw_scores = [87, 42, 96, 55, 71, 63]

total   = 0
passing = 0

for score in raw_scores:
    total += score
    if score >= 60:
        passing += 1

average = total / len(raw_scores)
print(f"Average: {average:.1f}")
print(f"Passing: {passing}/{len(raw_scores)}")
print(f"Top score: {sorted(raw_scores, reverse=True)[0]}")

ファイルのリストをソート順で処理し、大きすぎるものをスキップし、スキップされた数を報告します:

python
files = [
    {"name": "report_jan.csv", "size_mb": 12},
    {"name": "report_feb.csv", "size_mb": 850},
    {"name": "report_mar.csv", "size_mb": 7},
]

MAX_SIZE = 100
skipped  = 0

for f in sorted(files, key=lambda x: x["name"]):
    if f["size_mb"] > MAX_SIZE:
        print(f"Skipping {f['name']} ({f['size_mb']} MB, too large)")
        skipped += 1
    else:
        print(f"Processing {f['name']}...")

print(f"\nDone. {skipped} file(s) skipped.")

リクエストログをスキャンしてエラーを検出し、成功するか試行回数の上限に達したら終了するリトライループを使用します:

python
requests = [
    {"method": "GET",  "path": "/users",  "status": 200},
    {"method": "POST", "path": "/users",  "status": 201},
    {"method": "GET",  "path": "/broken", "status": 500},
]

errors = []

for req in requests:
    if req["status"] >= 400:
        errors.append(req)

if errors:
    print(f"{len(errors)} error(s) in request log:")
    for err in errors:
        print(f"  {err['method']} {err['path']} -> {err['status']}")
else:
    print("All requests succeeded")

attempts    = 0
max_retries = 3
success     = False

while attempts < max_retries and not success:
    attempts += 1
    print(f"Attempt {attempts}...")
    success = attempts >= 2   # 2回目の試行で成功をシミュレート

print("Connected" if success else "Failed after all retries")