Python の match 文が便利

## はじめに

Python 3.10 で導入された Match 文が便利だという話です。
この記事はPython 3.13 の環境で試しています。

## match 文とは

Match文は、Python 3.10で導入された構文であり、他の言語でいう switch 文、パターンマッチングに相当します。

```py
match value:
    case pattern1:
        # パターン1に一致した場合の処理
    case pattern2:
        # パターン2に一致した場合の処理
    case _:
        # それ以外の場合の処理
```

基本はこのようなものです。

<!--単にこれだけなら IF 文で良いんじゃないか？と思うかもしれませんが、いろいろな機能があります。 -->

## switch 文との違い？

:::note info
*作業中*
いろいろな言語に switch 文・式というのがある。それらとの違いを示して match 文の特徴を浮き彫りにする。
:::

- 要素
    - 素朴にリテラル・定数を対象に取る 
        - C言語、 JavaScript
    - 列挙型など多様なデータ構造を対象に取る
        - Python、 Ruby
    - 網羅性検査
        - Rust、 swift

とりあえず違いを考えてみたが、各々機能を拡張しているので長期的には大きな違いはなくなっていく気もする。

## サンプル1:  値を取り込む(API を叩く)

match 文のマッピングパターンでオブジェクトから値を取り込むことが出来ます。
例としてメトロポリタン美術館のAPIを叩いてみます。

```py
import json
import time
import requests


def met_api(query: str = "sunflowers") -> None:
    """ キーワード検索
    Document URL:https://metmuseum.github.io/
    """
    domain = "https://collectionapi.metmuseum.org"

    request = requests.get(
        f"{domain}/public/collection/v1/search?q={query}&=hasImages=true", timeout=10
    )

    match request.status_code:
        case 200:
            match json.loads(request.text):
                case {"total": total, "objectIDs": ids} if total > 0:
                    for id in ids:
                        print(f"https://www.metmuseum.org/art/collection/search/{id}")
        case _:
            pass
                             
```

## サンプル2: 型に基づく処理の分岐が容易になる(ポーランド記法の計算)

こんなふうに型に基づく処理の分岐ができます。

```py
match value:
    # | で OR を表す
    case int() | float():
        return value
    case _:
        raise ValueError("無効な式です")
```

これによって分岐が見やすくなり、値を取り込む機能と合わせて、次のようなことが簡単にできます。

```py
"""ポーランド記法の計算"""
type Pn = tuple[str, Pn|int|float, Pn|int|float]
def pn_calculator(expression: Pn|int|float) -> int | float:
    """再帰して式を計算する。これによって入れ子のタプルでも計算可能"""
    match expression:
        case int() | float():
            return expression
        case ("+", left, right):
            return pn_calculator(left) + pn_calculator(right)
        case ("-", left, right):
            return pn_calculator(left) - pn_calculator(right)
        case ("*", left, right):
            return pn_calculator(left) * pn_calculator(right)
        case ("/", left, right):
            return pn_calculator(left) / pn_calculator(right)
    raise ValueError("無効な式です")

print(pn_calculator(("+", 2, 3))) # 5
print(pn_calculator(("+", ("+",1,1), 3))) # 5
print(pn_calculator(("+", ("/",5.0,2), 3))) #5.5
```

## サンプル3:パターンマッチによる網羅性検査(型チェッカーに実装の漏れを検出させる)

Rust などには、分岐する可能性があるのを見逃しているとエラーを出す機能があります。
Python にも型チェッカーを利用すると同じようなことが出来ます。

- Python
    - [Unreachable Code and Exhaustiveness Checking — typing documentation](https://typing.readthedocs.io/en/latest/guides/unreachable.html)
        - このチャプターの元ネタ。英語

- Rust 
    - [match制御フロー演算子#マッチは包括的](https://doc.rust-jp.rs/book-ja/ch06-02-match.html#%E3%83%9E%E3%83%83%E3%83%81%E3%81%AF%E5%8C%85%E6%8B%AC%E7%9A%84)
        - 要するにどういうことができるか

```py
""" match case で網羅性検査 """

import enum
from typing import Never


def assert_never(arg: Never) -> Never:
    """網羅性チェック"""
    raise AssertionError("Expected code to be unreachable")


class SHIKOKU(enum.Enum):
    """四国の特産品を列挙したクラス"""

    KAGAWA = "うどん"
    TOKUSHIMA = "すだち"
    EHIME = "みかん"
    KOUCHI = "かつお"


def main(shikoku: SHIKOKU):
    """ Never 型による網羅性検査"""
    match shikoku:
        case shikoku.KAGAWA:
            return shikoku.KAGAWA.value
        case shikoku.TOKUSHIMA:
            return shikoku.TOKUSHIMA.value
        case shikoku.EHIME:
            return shikoku.EHIME.value
        # case が網羅されていないと下のような警告を出す
        # 型 "Literal[SHIKOKU.KOUCHI]" の引数を、関数 "assert_never" の型 "Never" のパラメーター "arg" に割り当てることはできません
        # 型 "Literal[SHIKOKU.KOUCHI]" は型 "Never" に割り当てできません
        #case shikoku.KOUCHI:
        #    return shikoku.KOUCHI.value
        case _:
            assert_never(shikoku)


if __name__ == "__main__":
    for cnt, i in enumerate(SHIKOKU):
        match cnt:
            case 0:
                print("ズルル〜")
            case _ if cnt > 0:
                print("パクパク")
        print(cnt, main(i))

```

## 終わりに

Match文がとても便利。
マッピングパターンが便利。
型に基づく分岐が簡潔にできて便利。

## Link

- [4. その他の制御フローツール — Python 3.13.0 ドキュメント](https://docs.python.org/ja/3.13/tutorial/controlflow.html#match-statements)
- [8. 複合文 (compound statement) — Python 3.13.0 ドキュメント](https://docs.python.org/ja/3/reference/compound_stmts.html#the-match-statement)