モダンPython中級 #6 パターンマッチングの深さ
基礎 #3 で match-case を軽く見ました。単純な分岐、OR パターン、構造分解、クラスマッチング、ガード までです。今回はその次のステップです。あらゆるパターン種別を一ヶ所に整理し、__match_args__ のようなユーザー定義との統合点、そしてよく陥る罠まで扱います。
5 つのパターンカテゴリ #
| 種類 | 形 | 意味 |
|---|---|---|
| リテラルパターン | case 200: | 正確な値の一致 |
| キャプチャパターン | case x: | 何でもマッチして変数にバインド |
| シーケンスパターン | case [a, b, *rest]: | list / tuple の形 |
| マッピングパターン | case {"key": v}: | dict の形 |
| クラスパターン | case Point(x=0): | クラス + 属性のマッチ |
この 5 つの組み合わせでほぼすべての場合を表現できます。一つずつ深く見ていきましょう。
リテラルと値の比較 #
リテラルパターンは == で比較されます。
match status:
case 200:
print("OK")
case "active":
print("active")
case True:
print("yes")
case None:
print("none")ドット表記 — 定数との比較 #
case ABC: のようにそのまま書くと キャプチャ変数 になります。定数比較をするには ドット (.) が必要 です。
SUCCESS = 200
match code:
case SUCCESS: # ⚠ SUCCESS という変数に code をバインド!
print("ok")
case _:
print("else")SUCCESS がすべての値にマッチしてしまい、2 つ目の case は絶対に実行されません。ツールが警告を出します。
class Status:
SUCCESS = 200
NOT_FOUND = 404
match code:
case Status.SUCCESS:
print("ok")
case Status.NOT_FOUND:
print("not found")定数をマッチングに使うときは enum またはクラスの属性 にしておくのが安全です。
from enum import Enum
class Status(Enum):
SUCCESS = 200
NOT_FOUND = 404
match status:
case Status.SUCCESS:
...
case Status.NOT_FOUND:
...キャプチャ変数 #
match command:
case x:
print(f"受け取ったコマンド: {x}")小文字 (正確にはドットがない識別子) はキャプチャです。すべての値にマッチして 変数にバインドします。_ (アンダースコア) だけ例外 — ワイルドカードなのでバインドしません。
キャプチャ変数を二度使えない #
match (a, b):
case (x, x): # ✗ SyntaxError
...同じ変数名を一つのパターン内で二度使うとエラーです。同じ値かどうかを検査するには ガード を使いましょう。
match (a, b):
case (x, y) if x == y:
...シーケンスパターン #
リストとタプルの 形 をパターンとして書けます。
match items:
case []:
print("空のリスト")
case [x]:
print(f"一つだけ: {x}")
case [x, y]:
print(f"二つ: {x}, {y}")
case [first, *rest]:
print(f"先頭 {first}、残り {rest}")
case [*init, last]:
print(f"前 {init}、最後 {last}")
case [first, *middle, last]:
print(f"{first}, {middle}, {last}")*name で残りを一度に受け取れて — 通常の unpacking と同じです。シーケンスパターンは list と tuple の両方にマッチします。 ただし str と bytes はシーケンスですがマッチングからは除外されます (意図しないマッチを防ぐため)。
match "hello":
case [a, *rest]:
print(a, rest) # マッチしない
case "hello":
print("hello!") # こちらに長さ固定 vs 可変 #
[a, b, c] — ちょうど長さ 3
[a, b, *rest] — 3 以上
[*items] — 任意の長さ (事実上キャプチャと同じ)
マッピングパターン #
dict の 部分マッチ です。書いたキーがすべてあればマッチして、それ以外のキーは無視されます。
event = {"type": "click", "x": 10, "y": 20, "extra": "ignored"}
match event:
case {"type": "click", "x": x, "y": y}:
print(f"({x}, {y})")
case {"type": "key", "code": code}:
print(f"key {code}")マッピングパターンは部分マッチ です。{"type": "click"} は「type キーが click の dict なら OK」。他のキーがあってもマッチします。
**rest で残りを受け取る
#
match config:
case {"host": host, "port": port, **rest}:
print(f"{host}:{port}、その他オプション: {rest}")キー自体はリテラルでなければならない #
match data:
case {key: value}: # ✗ key はキャプチャではない
...マッピングパターンの キーの位置は常にリテラルまたはドット表記 です。キャプチャできません。
クラスパターン #
基礎 #3 で短く見た形:
class Circle:
def __init__(self, radius: float):
self.radius = radius
match shape:
case Circle(radius=r):
print(f"円、半径 {r}")キーワード引数の形でマッチします。属性名 を使うのです。
__match_args__ — 位置マッチング
#
Point(0, 0) のように 位置引数 でマッチするには、クラスが __match_args__ を教える必要があります。
class Point:
__match_args__ = ("x", "y")
def __init__(self, x: float, y: float):
self.x = x
self.y = y
match p:
case Point(0, 0):
print("原点")
case Point(0, y):
print(f"y 軸上、y={y}")
case Point(x, 0):
print(f"x 軸上、x={x}")
case Point(x, y):
print(f"({x}, {y})")@dataclass が自動生成
#
#1 の @dataclass が __match_args__ を 自動で作ってくれます。 だから dataclass とパターンマッチングは相性抜群です。
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
match p:
case Point(0, 0): # 自動で動作
print("原点")ビルトインもマッチング可能 — ただし引数は一つだけ #
int、str、bool のようなビルトインは 位置一つ で値そのものをマッチします。
match x:
case int(n):
print(f"整数 {n}")
case str(s):
print(f"文字列 {s}")
case list() if not x: # 空のリスト
print("空のリスト")int(n) は「int な値を n にキャプチャ」という意味です。
キャプチャ vs 比較 — as キーワード
#
マッチした値全体を変数で受け取りたいときは as を使います。
match shape:
case Circle() as c:
register(c)
case Square(side=s) as sq if s > 10:
register(sq)元のものをそのままもう一度参照する必要がある場面で有用です。
OR パターン — |
#
match status:
case 200 | 201 | 204:
print("成功")
case 400 | 404 | 422:
print("クライアントエラー")OR パターンの中では すべての枝が同じ変数をキャプチャ しなければなりません。
match val:
case [x] | [x, y]: # ✗ y は最初のパターンにない
...match val:
case [x] | [x, _]: # 両方とも x だけキャプチャ
...ガード — if
#
基礎 #3 で短く見ました。
match (x, y):
case (a, b) if a == b:
...
case (a, b) if a > b:
...ガードは すべてのパターン種別と組み合わせ可能 です。キャプチャされた変数を見て追加条件を検査するところです。
ワイルドカード — _
#
_ は キャプチャなしでマッチ します。case _: は default の役割。
match status:
case 200:
print("OK")
case _:
print("その他") # defaultまた 複数の箇所に来ても衝突しません。
match items:
case [_, _, third]:
print(f"3 つ目: {third}")x, x はダメですが _, _ はできます — _ はキャプチャをしないからです。
組み合わせ例 — JSON 処理 #
API レスポンスをさまざまな形に応じて分岐するケース。
def handle(response: dict) -> str:
match response:
# 成功ケース — データがあって type が決まった形
case {"status": "ok", "data": {"items": list(items), "total": int(total)}}:
return f"受信: {len(items)} 件 / 全体 {total}"
# ページネーションがある場合
case {"status": "ok", "data": {"items": items, "next_cursor": str(cursor)}}:
return f"次ページ cursor: {cursor}"
# 空の結果
case {"status": "ok", "data": {"items": []}}:
return "結果なし"
# エラー — コード + メッセージ
case {"status": "error", "code": int(code), "message": str(msg)}:
return f"エラー {code}: {msg}"
# その他
case _:
return "不明なレスポンス"同じことを if/elif で書くと 型検査 + キーの存在検査 + 値のキャプチャがバラバラ に散ります。match がその 3 つを一行にまとめてくれます。
match-case が合わない場合
#
万能ではありません。単純な if の方が読みやすいケース も多くあります。
match score:
case s if s >= 90:
grade = "A"
case s if s >= 80:
grade = "B"
case s if s >= 70:
grade = "C"
case _:
grade = "F"if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 70:
grade = "C"
else:
grade = "F"判断基準: 形で分岐するのか、値の範囲で分岐するのか。 形がカギなら match、単純な比較なら if/elif が普通は良いです。
まとめ #
今回まとめたこと:
- 5 つのパターン — リテラル、キャプチャ、シーケンス、マッピング、クラス
- ドット表記 (
Status.OK) で定数比較、単純な名前はキャプチャ - シーケンス:
[a, *rest, b]、str / bytes はマッチ除外 - マッピング: 部分マッチ、キーはリテラル、
**restで残り - クラスパターン:
__match_args__で位置マッチング、dataclass が自動生成 - ビルトインのマッチング:
int(n)、str(s)の形 asで全体の値をキャプチャ|で OR (すべての枝が同じ変数だけキャプチャ)- ガード
ifはすべてのパターンと組み合わせ可能 _はキャプチャなし、複数の箇所で重複可能- 形で分岐は match、値の範囲で分岐は if/elif
次回 (#7 非同期入門) が中級シリーズの最後 — asyncio と async/await を扱います。上で短く見たコルーチンが本格的に登場するところです。