Pythonのmatch文でパターンマッチングをマスターする

Shunku

Python 3.10で導入されたmatch文は、値の構造に基づいて分岐する強力な機能です。単純なswitch文を超えて、シーケンス、マッピング、クラスインスタンスなど、複雑なデータ構造を分解しながらマッチングできます。

基本構文

def http_status(status: int) -> str:
    match status:
        case 200:
            return "OK"
        case 404:
            return "Not Found"
        case 500:
            return "Internal Server Error"
        case _:
            return "Unknown"

_はワイルドカードで、どんな値にもマッチします(他のcaseにマッチしなかった場合のデフォルト)。

パターンの種類

リテラルパターン

特定の値に完全一致:

def describe_value(value):
    match value:
        case 0:
            return "zero"
        case 1:
            return "one"
        case True:  # 注意: True == 1 なので順序が重要
            return "true"
        case "hello":
            return "greeting"
        case None:
            return "nothing"

キャプチャパターン

変数に値をバインド:

def process(command: str) -> str:
    match command.split():
        case ["quit"]:
            return "Goodbye!"
        case ["hello", name]:
            return f"Hello, {name}!"
        case ["add", x, y]:
            return f"Result: {int(x) + int(y)}"
        case _:
            return "Unknown command"

print(process("hello World"))  # Hello, World!
print(process("add 5 3"))      # Result: 8

シーケンスパターン

リストやタプルをマッチング:

def analyze_sequence(seq):
    match seq:
        case []:
            return "empty"
        case [x]:
            return f"single: {x}"
        case [x, y]:
            return f"pair: {x}, {y}"
        case [first, *rest]:
            return f"first: {first}, rest: {rest}"

print(analyze_sequence([]))           # empty
print(analyze_sequence([1]))          # single: 1
print(analyze_sequence([1, 2]))       # pair: 1, 2
print(analyze_sequence([1, 2, 3, 4])) # first: 1, rest: [2, 3, 4]

マッピングパターン

辞書をマッチング:

def handle_event(event: dict):
    match event:
        case {"type": "click", "x": x, "y": y}:
            return f"Click at ({x}, {y})"
        case {"type": "keypress", "key": key}:
            return f"Key pressed: {key}"
        case {"type": "scroll", "direction": d, **rest}:
            return f"Scroll {d}, extra: {rest}"
        case _:
            return "Unknown event"

print(handle_event({"type": "click", "x": 100, "y": 200}))
# Click at (100, 200)

print(handle_event({"type": "scroll", "direction": "up", "speed": 5}))
# Scroll up, extra: {'speed': 5}

クラスパターン

オブジェクトの属性をマッチング:

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

@dataclass
class Circle:
    center: Point
    radius: float

@dataclass
class Rectangle:
    top_left: Point
    width: float
    height: float

def area(shape) -> float:
    match shape:
        case Circle(center=_, radius=r):
            return 3.14159 * r * r
        case Rectangle(width=w, height=h):
            return w * h
        case Point():
            return 0.0
        case _:
            raise ValueError(f"Unknown shape: {shape}")

print(area(Circle(Point(0, 0), 5)))      # 78.53975
print(area(Rectangle(Point(0, 0), 3, 4))) # 12.0

ガード条件

ifでマッチに追加条件を指定:

def classify_number(n: int) -> str:
    match n:
        case 0:
            return "zero"
        case x if x < 0:
            return "negative"
        case x if x % 2 == 0:
            return "positive even"
        case _:
            return "positive odd"

print(classify_number(-5))  # negative
print(classify_number(4))   # positive even
print(classify_number(7))   # positive odd

複合ガード

def validate_user(user: dict) -> str:
    match user:
        case {"name": name, "age": age} if age >= 18 and len(name) > 0:
            return f"Valid adult: {name}"
        case {"name": name, "age": age} if age < 18:
            return f"Minor: {name}"
        case {"name": ""}:
            return "Empty name"
        case _:
            return "Invalid user"

ORパターン

複数のパターンを|で組み合わせ:

def day_type(day: str) -> str:
    match day.lower():
        case "saturday" | "sunday":
            return "weekend"
        case "monday" | "tuesday" | "wednesday" | "thursday" | "friday":
            return "weekday"
        case _:
            return "invalid"

print(day_type("Saturday"))  # weekend
print(day_type("Monday"))    # weekday

ステータスコードの分類

def classify_status(code: int) -> str:
    match code:
        case 200 | 201 | 204:
            return "success"
        case 301 | 302 | 307 | 308:
            return "redirect"
        case 400 | 401 | 403 | 404:
            return "client error"
        case 500 | 502 | 503:
            return "server error"
        case _:
            return "unknown"

ASパターン

パターン全体を変数にバインド:

def process_point(point):
    match point:
        case (x, y) as p if x == y:
            return f"Diagonal point: {p}"
        case (0, y) as p:
            return f"On Y-axis: {p}"
        case (x, 0) as p:
            return f"On X-axis: {p}"
        case p:
            return f"General point: {p}"

print(process_point((5, 5)))   # Diagonal point: (5, 5)
print(process_point((0, 10)))  # On Y-axis: (0, 10)

ネストしたパターン

複雑な構造を再帰的にマッチング:

def process_json(data):
    match data:
        case {"users": [{"name": name, "role": "admin"}, *_]}:
            return f"First admin: {name}"
        case {"users": [first, second, *rest]}:
            return f"First two users: {first}, {second}"
        case {"error": {"code": code, "message": msg}}:
            return f"Error {code}: {msg}"
        case {"data": {"items": [*items]}} if len(items) > 0:
            return f"Found {len(items)} items"
        case _:
            return "Unknown structure"

data = {
    "users": [
        {"name": "Alice", "role": "admin"},
        {"name": "Bob", "role": "user"}
    ]
}
print(process_json(data))  # First admin: Alice

実践的なパターン

コマンドパーサー

def execute(command: list[str]) -> str:
    match command:
        case ["exit" | "quit" | "q"]:
            return "Exiting..."
        case ["help"]:
            return "Available commands: help, list, add, remove"
        case ["list"]:
            return "Listing items..."
        case ["add", item]:
            return f"Adding: {item}"
        case ["add", *items] if len(items) > 1:
            return f"Adding multiple: {', '.join(items)}"
        case ["remove", item]:
            return f"Removing: {item}"
        case [cmd, *_]:
            return f"Unknown command: {cmd}"
        case []:
            return "No command provided"

APIレスポンスハンドリング

from typing import Any

def handle_response(response: dict[str, Any]) -> str:
    match response:
        case {"status": "success", "data": data}:
            return f"Success: {data}"
        case {"status": "error", "error": {"code": code, "message": msg}}:
            return f"Error {code}: {msg}"
        case {"status": "pending", "retry_after": seconds}:
            return f"Retry after {seconds} seconds"
        case {"status": status}:
            return f"Unknown status: {status}"
        case _:
            return "Invalid response format"

式ツリーの評価

from dataclasses import dataclass
from typing import Union

@dataclass
class Num:
    value: float

@dataclass
class Add:
    left: "Expr"
    right: "Expr"

@dataclass
class Mul:
    left: "Expr"
    right: "Expr"

Expr = Union[Num, Add, Mul]

def evaluate(expr: Expr) -> float:
    match expr:
        case Num(value=v):
            return v
        case Add(left=l, right=r):
            return evaluate(l) + evaluate(r)
        case Mul(left=l, right=r):
            return evaluate(l) * evaluate(r)

# (2 + 3) * 4 = 20
expr = Mul(Add(Num(2), Num(3)), Num(4))
print(evaluate(expr))  # 20.0

注意点とベストプラクティス

パターンの順序

より具体的なパターンを先に:

# 正しい順序
match value:
    case [1, 2, 3]:        # 具体的
        pass
    case [1, 2, x]:        # やや具体的
        pass
    case [1, *rest]:       # 一般的
        pass
    case _:                # 最も一般的
        pass

定数の使用

定数はドット付きの名前で参照:

class HttpStatus:
    OK = 200
    NOT_FOUND = 404

def handle_status(code: int) -> str:
    match code:
        case HttpStatus.OK:      # 定数として認識
            return "OK"
        case HttpStatus.NOT_FOUND:
            return "Not Found"
        case _:
            return "Unknown"

if-elifとの使い分け

flowchart TD
    Q1{"データ構造を<br/>分解する?"}
    Q2{"複数の値を<br/>同時にチェック?"}
    Q3{"ネストした<br/>構造?"}

    Q1 -->|Yes| Match["Match Statement"]
    Q1 -->|No| Q2
    Q2 -->|Yes| Match
    Q2 -->|No| Q3
    Q3 -->|Yes| Match
    Q3 -->|No| IfElif["if-elif"]

    style Match fill:#3b82f6,color:#fff
    style IfElif fill:#22c55e,color:#fff

まとめ

match文は、Pythonに強力なパターンマッチングをもたらします:

パターン 用途
リテラル 特定の値にマッチ
キャプチャ 値を変数にバインド
シーケンス リスト/タプルを分解
マッピング 辞書を分解
クラス オブジェクト属性を分解
OR (|) 複数パターンを組み合わせ
AS パターン全体をキャプチャ
ガード 追加条件を指定

主要な原則:

  • 構造を分解する: シンプルな値比較ならif-elif、構造分解ならmatch
  • 具体的なパターンを先に: より限定的なcaseを上に配置
  • ガードを活用: パターンだけでは表現できない条件を追加
  • ワイルドカードで安全に: case _でフォールバックを用意

match文は、複雑なデータ構造の処理を宣言的かつ読みやすく記述できる、モダンPythonの必須機能です。

参考資料