match と case によるモダンなロジック
2021年末にPython 3.10が登場する前、複雑な分岐ロジックの処理は、通常 if-elif-else ステートメントを入れ子にするか、辞書ディスパッチテーブルを使用することを意味していました。構造的パターンマッチング(Structural Pattern Matching)がそれを変えました。これは単なる洗練された「switch」文ではありません。データの「デストラクチャ(構造分解)」を可能にし、データの形状をチェックして値を抽出するという作業を一段階で行うことができます。
かつて私は、レガシーAPIの1,500行に及ぶメッセージブローカーを整理するのに週末を費やしたことがあります。ネストされた if ブロックがあまりにも深く、ロジックを読むためだけに横にスクロールしなければならないほどでした。match-case に切り替えたことで, その特定のモジュールの行数は30%削減され、チームの他のメンバーにとってもロジックが即座に理解できるものになりました。
クイックスタート:if-else から match へ
まずは簡単な例として、基本的なシステムコマンドの処理から始めましょう。以前なら、次のような「はしご型」のコードを書いていたかもしれません。
def handle_command(command):
if command == "quit":
return "終了しています..."
elif command == "reset":
return "システムをリセットしました。"
else:
return "不明なコマンドです。"
match-case を使えば、コードは選択肢のリストのように読めます:
def handle_command(command):
match command:
case "quit":
return "終了しています..."
case "reset":
return "システムをリセットしました。"
case _:
return "不明なコマンドです。"
そのアンダースコア(_)は「キャッチオール(すべてに一致)」です。上の特定のケースに一致しなかったあらゆる入力を処理します. クリーンで読みやすく、ロジックをフラットに保つことができます。
仕組みを理解する:デストラクチャリング
シーケンス(順序集合)のマッチングこそが、この機能の真骨頂です。Pythonはリストやタプルの中身を確認し、それらが特定のテンプレートに適合するかどうかをチェックできます。これにより、手動でのインデックスチェックやスライシングが不要になります。
シーケンスのマッチング
引数が1つ、2つ、あるいは3つあるかもしれないシェルコマンドを処理することを想像してください。len(args) をチェックして args[1] にアクセスする代わりに、このように記述できます:
def execute(action):
match action.split():
case ["load", filename]:
print(f"{filename} を読み込んでいます...")
case ["move", x, y]:
print(f"座標 ({x}, {y}) へ移動中")
case ["quit"]:
print("さようなら!")
case _:
print("無効な入力です。")
Pythonは単に値をチェックするだけでなく、その場で変数にバインド(割り当て)します。["load", filename] のケースでは、最初の単語が “load” であれば、2番目の単語は自動的に filename に割り当てられます。これは「キャプチャパターン」として知られています。
辞書のマッチング
これは、StripeのウェブフックやGitHubのイベントのようなAPIレスポンスを処理する際に私が重宝している方法です。キーの検証と値の取得を同時に行うことができます。
def process_api_response(response):
match response:
case {"status": "error", "details": {"message": msg}}:
print(f"エラーログ: {msg}")
case {"status": "success", "data": data_content}:
process_data(data_content)
case _:
print("予期しない形式を受信しました。")
辞書のマッチングは、デフォルトで「部分一致」です。{"status": "success"} のようなパターンは、そのキーが含まれていれば、他に50個のフィールドがあってもマッチします。ノイズを無視して、必要な情報だけに集中できます。
高度なパターンとガード
マッチングは基本的なリストや辞書に限定されません。カスタムクラスに適用したり、ロジックフィルタを追加したりすることもできます。
クラスパターン
オブジェクトが特定のクラスのインスタンスであるかを確認し、その属性を検査できます。Point クラスがある場合、次のようにフィルタリングできます:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def locate_point(point):
match point:
case Point(x=0, y=0):
print("原点")
case Point(x=x, y=0):
print(f"X軸上の {x}")
case Point(x=0, y=y):
print(f"Y軸上の {y}")
case Point(x=x, y=y):
print(f"点 ({x}, {y})")
パターンガード
ガードは「最終的なフィルタ」のようなものだと考えてください。構造的な一致だけでは不十分で、さらに論理的な条件が必要な場合があります。
def check_age(person):
match person:
case {"name": name, "age": age} if age < 18:
print(f"{name} は未成年です。")
case {"name": name, "age": age}:
print(f"{name} は成人です。")
if age < 18 がガードです。これは構造的なマッチングが成功した後にのみ実行されます。これによりロジックがフラットに保たれ、caseの内部に深くネストされた if 文が作られるのを防ぎます。
実践的なアドバイス:落とし穴を避ける
いくつかの本番用マイクロサービスで match-case を実装した結果、よくあるバグを防ぐためのいくつかのルールを見つけました。
1. オーバーエンジニアリングを避ける
データの構造が重要な場合や、3つ以上の分岐がある場合に使用してください。単一の if-else チェックであれば、match-case は過剰です。新しい構文だからといって、単純なロジックに無理に押し込まないでください。
2. 順序がすべて
パターンは上から下へとチェックされます。具体的なパターンを先に記述しなければなりません。一番上に広範なワイルドカードや一般的なパターンを置くと、その下の特殊なケースは決して実行されません。これは、try-catch文で except ブロックを並べるロジックと同じです。
3. 類似のロジックをグループ化する
パイプ記号(|)を使用すると、複数の入力を一つの case にまとめることができます。同じ return 文を3回繰り返すよりもずっとクリーンです。
case "quit" | "exit" | "bye":
close_connection()
4. 定数の落とし穴
STATUS_ERROR = 404 のような定数を case STATUS_ERROR: でマッチさせようとするのは、よくある罠です。Pythonはこれを新しい変数名として扱います。比較ではなく、値の代入が行われてしまいます。これを解決するには、Enum(列挙型)を使用するか、constants.STATUS_ERROR のようにドット付きの名前を使用します。
5. パフォーマンスは文脈次第
パフォーマンスが問題になることは稀です。数百万回実行されるタイトなループ内では、辞書ルックアップの方が技術的には高速です。しかし、ウェブやデータアプリケーションの99%において、可読性の向上は数マイクロ秒の実行時間よりもはるかに価値があります。
構造的パターンマッチングは、Pythonにおけるデータ処理方法の根本的な転換です。個々の値だけでなくデータの「形状」に注目することで、堅牢でテストしやすく、同僚にとっても理解しやすいコードを書くことができます。

