モダンPython基礎 #3 制御フロー — if, while, for, match-case

読了 6分

#2 変数、基本型、型ヒント で作った変数たちを使い、今回は 流れ を作っていきます。ifwhilefor、そして 3.10 で入った match-case までです。

Python の制御フローには大きな特徴が二つあります。

  1. ブロックはインデントで区切ります。 中括弧 {} はありません。
  2. コロン (:) でヘッダを終えます。 if x > 0: のように。

つまりインデントがそのまま文法です。一般的には 半角スペース 4 つ を使います (PEP 8)。タブとスペースを混ぜるとエラーになります。

if / elif / else #

他の言語と最も似ています。括弧がないのが違いです。

if 基本
score = 85

if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 70:
    grade = "C"
else:
    grade = "F"

print(grade)   # B

else if ではなく elif です。なぜ合体しているのかは歴史的な理由で、覚えてしまえば十分です。

一行で書く — 三項演算子 #

C 系の cond ? a : b とは違います。Python は a if cond else b の形です。

三項
status = "成人" if age >= 19 else "未成年"

自然言語に近い順序なので最初は違和感がありますが、慣れれば読みやすくなります。

真理値と条件 — falsy の活用 #

#2 で見た falsy 値をそのまま活用します。

falsy 条件
items: list[int] = []

if not items:
    print("空です")    # これが出力される

# 下も同じ意味だが、上の方が Pythonic
if len(items) == 0:
    print("空です")

リストが空かどうかの確認に len(x) == 0 を使うのは、普通は not x に縮めます。

is== #

これは注意が必要な部分です。

is と == の違い
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b)   # True   (値が同じ)
print(a is b)   # False  (別のオブジェクト)

x = None
print(x is None)   # True  ← None 比較は常に is

== は値の比較、is はオブジェクトの同一性比較 です。NoneTrueFalse は常に is で比較してください。それ以外の値は == です。

while #

条件が真の間、繰り返します。

while 基本
count = 0
while count < 5:
    print(count)
    count += 1

++ はありません。count += 1 です。

breakcontinue #

break / continue
n = 0
while True:
    n += 1
    if n % 2 == 0:
        continue       # 偶数はスキップ
    if n > 7:
        break          # 7 を超えたら終了
    print(n)
# 1, 3, 5, 7

while True + break は「条件が複雑で上に書きづらい」無限ループのパターンです。よく使います。

else 節 — あまり知られていない機能 #

whilefor には else を付けることができます。 「ループが break なしで終わったとき」に実行されます。

while-else
n = 0
while n < 10:
    if n == 99:
        break
    n += 1
else:
    print("break なしで終わった")    # これが出力

頻繁ではありませんが「探している値がなくて全部回ったとき」を処理するのに、すっきりはまる場面があります。使いすぎると逆に可読性が落ちるので、ほどほどに。

for — すべてシーケンスの巡回 #

他の言語の for (i = 0; i < n; i++) のような C スタイルは ありません。 Python の for は常に イテラブルを巡回 します。

for 基本
names = ["a", "b", "c"]
for name in names:
    print(name)

文字列もイテラブルです。

一文字ずつ
for ch in "hello":
    print(ch)
# h, e, l, l, o

range — 数値のシーケンスが必要なとき #

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

for i in range(2, 7):    # 2, 3, 4, 5, 6
    print(i)

for i in range(0, 10, 2):  # 0, 2, 4, 6, 8 (step 2)
    print(i)

range(start, stop, step) です。stop含まれません (半開区間)。最初は戸惑いますが、他の言語のスライスと同じ慣例なのですぐ慣れます。

enumerate — インデックスが必要なとき #

インデックスが必要なとき
items = ["りんご", "梨", "柿"]
for i, item in enumerate(items):
    print(f"{i}: {item}")
# 0: りんご
# 1: 梨
# 2: 柿

for i in range(len(items)) のあとに items[i] で取り出すのはアンチパターンです。enumerate の一語で済みます。

zip — 複数のリストを同時に巡回 #

zip
names = ["カーティス", "スミス", "ジョン"]
ages = [30, 25, 40]

for name, age in zip(names, ages):
    print(f"{name}: {age}")

長さが違うと短い方に合わせて切れます。長さが合わないときにエラーを出したければ zip(..., strict=True) (3.10+) です。

strict モード
for n, a in zip(names, ages, strict=True):
    ...

パターンマッチング — match-case (3.10+) #

switch と見た目は似ていますが、できることはずっと多いです。 Python は単純な分岐ではなく パターンマッチング として設計されています。

最も単純な形 #

単純な分岐
def http_status(code: int) -> str:
    match code:
        case 200:
            return "OK"
        case 404:
            return "Not Found"
        case 500:
            return "Server Error"
        case _:
            return "Unknown"

case _: がワイルドカード (default) です。JavaScript の default: の位置です。

複数の値を一つのケースに — | #

OR パターン
def category(code: int) -> str:
    match code:
        case 200 | 201 | 204:
            return "成功"
        case 400 | 401 | 403 | 404:
            return "クライアントエラー"
        case 500 | 502 | 503:
            return "サーバーエラー"
        case _:
            return "その他"

構造分解 — ここからが本領 #

リスト、タプル、辞書の 形そのものをパターンとして 使えます。

構造分解
def describe(point: tuple[int, ...]) -> str:
    match point:
        case ():
            return "空の点"
        case (x,):
            return f"1D: {x}"
        case (x, y):
            return f"2D: ({x}, {y})"
        case (x, y, z):
            return f"3D: ({x}, {y}, {z})"
        case _:
            return "高次元"

print(describe((1, 2)))      # 2D: (1, 2)
print(describe((1, 2, 3)))   # 3D: (1, 2, 3)

各パターン内の xyその位置の値を変数にバインド します。マッチと同時に変数が作られます。JavaScript の構造分解 + switch が一箇所に合わさったような感覚です。

辞書パターン #

dict パターン
event = {"type": "click", "x": 100, "y": 200}

match event:
    case {"type": "click", "x": x, "y": y}:
        print(f"クリック: ({x}, {y})")
    case {"type": "key", "code": code}:
        print(f"キー: {code}")
    case _:
        print("不明なイベント")

特定のキーが存在し、その値を変数として取り出すパターン。API レスポンス処理に本当によく合います。

ガード — if 条件の追加 #

ガード
match (x, y):
    case (a, b) if a == b:
        print(f"同じ: {a}")
    case (a, b) if a > b:
        print(f"a が大きい")
    case _:
        print("その他")

クラスパターン #

クラスマッチング
class Circle:
    def __init__(self, radius: float):
        self.radius = radius

class Square:
    def __init__(self, side: float):
        self.side = side

shape = Circle(5)

match shape:
    case Circle(radius=r):
        print(f"円、半径 {r}")
    case Square(side=s):
        print(f"正方形、一辺 {s}")

型チェックと属性抽出が一行に収まります。

switch と趣が異なる理由 #

match-case は単純な多重分岐ではなく データの形で分岐するための道具 です。上のコードを if-elif で書き直すと、長く不格好になります。

match なしで同じこと
if isinstance(shape, Circle):
    r = shape.radius
    print(f"円、半径 {r}")
elif isinstance(shape, Square):
    s = shape.side
    print(f"正方形、一辺 {s}")

同じ動作ですが 型チェックと属性取り出しがバラバラ です。match はこの二段階を合わせて一行で表現します。JSON / イベントオブジェクト処理で効果が大きくなります。

流れの真実 — pass #

ブロックを空にすることはできません。インデントブロックがないと文法エラーです。とりあえず枠だけ確保したいときに pass を使います。

枠だけ確保
def todo(): 
    pass    # 後で実装

if x > 0:
    pass    # ひとまず無視
else:
    handle(x)

... (Ellipsis オブジェクト) も同じ位置でよく見かけますが、pass の方が慣用的です。

for にも内包表記というパターンがある #

リスト、辞書、セットを作るとき、for を一行に圧縮する文法があります。

簡単なプレビュー
squares = [x ** 2 for x in range(5)]
# [0, 1, 4, 9, 16]

これは次の記事 (#4 コレクションと内包表記) で本格的に扱います。

まとめ #

今回見たもの:

  • インデントでブロック — 半角スペース 4 つ、コロンでヘッダ終了
  • if / elif / else、三項 a if cond else b
  • is はオブジェクト同一性、== は値の比較、None チェックは is None
  • whilebreak / continue / else
  • for in でイテラブルを巡回 — rangeenumeratezip
  • match-case — 単純分岐、OR パターン、構造分解、dict パターン、ガード、クラスマッチング
  • 空のブロックには pass

次回 (#4 コレクションと内包表記) では、最もよく使う四つのデータ構造 — listtupledictset と、それらを一行で作る 内包表記 を扱います。

X