[中級者向け] Python 構造的パータンマッチの 10 のパターン

# はじめに

比較的新しい機能である構造的パターンマッチ文 (以下、マッチ文) を解説します。
初学者はまず if 文で条件分岐を身に付けるべきと考え、タイトルでは中級者向けと謳っています。

:::note warn
Python 3.10 以降の機能です
:::

# マッチ文の概要

他言語における switch 文のようなものです。

```python:if 文での条件分岐
a = 0

if a == 0:
    print("0")
elif a == 1:
    print("1")
else:
    print("other")

--> 0
```

```python:マッチ文での条件分岐
a = 0

match a:
    case 0:
        print("0")
    case 1:
        print("1")
    case _:
        print("other")

--> 0
```

マッチ文を定義する手順

1. `match` の後ろに値を記述して、`:` で区切る
2. `case` の後ろに値を記述して、`:` で区切る
3. 分岐の数だけ 2. を繰り返す
4. どの条件にも当てはまらない処理は `case _:` の後に記述

Java の switch 文では分岐が多いと記述が簡潔になるとのことですが、Python はもとより if 文の記述が簡潔なため、マッチ文のメリットが薄いように思えます。

ですが以下のケースに当てはまる状況下では、その恩恵を享受できます。

## OR パターン

`|` で要素を列挙するパターンです。

```python:複数のパターンを併記
mob = "villager"

match mob:
    case "zombie" | "skeleton" | "creeper":
        print("Hostile")
    case "chicken" | "pig" | "villager":
        print("Passive")
    case "enderman" | "piglin":
        print("Neutral")

--> Passive
```

if 文でいうところの `or` で繋げるイメージです。

## AS パターン

`as` で条件を満足した要素と変数を束縛するパターンです。

```python:変数への束縛
mob = "villager"

match mob:
    case "zombie" | "skeleton" | "creeper" as hostile:
        print(hostile)
    case "chicken" | "pig" | "villager" as passive:
        print(passive)
    case "enderman" | "piglin" as neutral:
        print(neutral)

--> villager
```

ビルトインクラストのマッチは [こちら](#マッピングパターン) を参照してください。

## リテラルパターン

リテラルを利用した一般的な例です。
内部的には `==` (真偽値や `None` は `is`) で評価しています。

```python:だいたいのリテラルが利用できる
val = None

match val:
    case 0:
        print(0)
    case "abc":
        print("abc")
    case """abcabc""":
        print("abcabc")
    case r"\t\n":
        print(r"\t\n")
    case b"abc":
        print("byte-abc")
    case True:
        print(True)
    case False:
        print(False)
    case None:
        print(None)
    case _:
        print("other")

--> None
```

数値 (複素数)・文字列・真偽値・`None` が利用できますが、式と f 文字列は記述できません。

```python:式は利用できない
val = 2

match val:
    case 1 + 1:
        print(2)
    case _:
        print("other")

--> SyntaxError
```

ちなみに複素数の場合のみ、`+` や `-` を利用できます。
複素数を使うタイミングなどほとんどないとは思いますが...

```python:複素数
val = 1 + 2j

match val:
    case 1 + 2j:
        print("1+2j")
    case 1 - 2j:
        print("1-2j")

--> 1+2j
```

ちなみに実数は左側で虚数は右側と決まっています。

```python:実数と虚数の位置は変更できない
val = 1 + 2j

match val:
    case 2j + 1:
        print("1+2j")
    case -2j + 1:
        print("1-2j")

--> SyntaxError
```

## キャプチャパターン

パターンに変数を指定することで、常にマッチを成功させ値をその変数に束縛します。

```python:値を変数に束縛
mob = "slime"

match mob:
    case name:
        print(name)

--> slime
```

後述するパターンと組み合わせることで、真価を発揮します。

## ワイルドカードパターン

前述した `_` のことです。
最後に記述することで、`else` のように振舞います。

```python:値を変数に束縛しない
mob = "slime"

match mob:
    case _:
        print(_)
```

注意点はキャプチャパターンより優先されるため、`_` に値が束縛されないことぐらいでしょうか。
つまり上では `case _` の `_` と `mob` は紐づいていませんし、そもそも `case _` と `print(_)` の `_` は別物なので、なにも表示されません。

## バリューパターン

オブジェクトの属性と値を比較します。
`.` が付いていると、バリューパターンと認識されます。

```python:オブジェクトの属性と比較
class Pokemon:
    def __init__(self, name):
        self.name = name


pikachu = Pokemon("Pikachu")

name = "Pikachu"

match name:
    case pikachu.name:
        print(name)

--> Pikachu
```

## グループパターン

`()` でパターンの可読性を高めるだけです。

```python:長いパターン
tool = "chainsaw"

match tool:
    case "auger" | "chainsaw" | "impact driver":
        print(tool)

--> chainsaw
```

```python:グルーピングしたパターン
match tool:
    case ("auger" | "chainsaw" | "impact driver"):
        print(tool)

--> chainsaw
```

この程度の構文であれば、可読性に差は感じません。
AS パターンを併用した例で確認すると、効果を実感できそうです。

```python:グルーピングで可読性を向上
mob = "villager"

match mob:
    case ("zombie" | "skeleton" | "creeper") as hostile:
        print(hostile)
    case ("chicken" | "pig" | "villager") as passive:
        print(passive)
    case ("enderman" | "piglin") as neutral:
        print(neutral)
```

## シーケンスパターン

リストやタプルといったシーケンス系にマッチするパターンです。

```python:単純なリストのマッチ
lst_1 = [0, 1, 2]

match lst_1:
    case [0, 1, 2]:
        print(lst_1)

--> [0, 1, 2]
```

```python:単純なタプルのマッチ
tup_1 = (0, 1, 2)

match tup_1:
    case (0, 1, 2):
        print(tup_1)

--> (0, 1, 2)
```

これでは if 文で事足りますが、下のようなケースではかなり簡潔に記述できます。

```python:要素数が 2 で 0 から始まるリスト
lst_2 = [0, 1]

match lst_2:
    case [0, x]:
        print(x)

--> 1
```

`[0, x]` は `0` から始まる 2 要素のリストであれば、次の要素を `x` に束縛するという意味です。
キャプチャパターンとの合わせ技になっているのが**ミソ**です。

```python:上を if で
lst_2 = [0, 1]

if len(lst_2) == 2 and lst_2[0] == 0:
    print(lst_2[1])

--> 1
```

どうでしょう、マッチ文のほうがかなり読みやすくはないでしょうか?
要素を増やしたもう少し複雑な例も確認します。

```python:要素数が 3 で 0, 1 から始まるリスト
lst_1 = [0, 1, 2]

match lst_1:
    case [0, 1, x]:
        print(x)

--> 2
```

```python:上を if で
lst_1 = [0, 1, 2]

if len(lst_1) == 3:
    if lst_1[0] == 0 and lst_1[1] == 1:
        print(lst_1[2])

--> 2
```

このレベルだと if 文の可読性は酷いものです。

:::note
if 文と `len()` を同時に利用する場合は、ぜひマッチ文も検討してください
:::

### 複数の変数

パターン内で複数の変数を利用する際には、別の変数を記述してください。

```python:複数の変数
lst_1 = [0, 1, 2]

match lst_1:
    case [0, x, y]:
        print(x)
        print(y)

--> 1
    2
```

### 可変長の要素

関数定義と同様に、要素が可変長であれば `*` を記述します。

```python:可変長の要素
lst_1 = [0, 1, 2]

match lst_1:
    case [0, *values]:
        for value in values:
            print(value)

--> 1
    2
```

<a id="マッピングパターン"></a>

## マッピングパターン

ディクショナリのようなマッピング系にマッチするパターンです。

```python:単純なディクショナリのマッチング
mob = {"name": "ghast", "health": 10, "width": 4.0}

match mob:
    case {"name": "ghast"}:
        print(mob)

--> {"name": "ghast", "health": 10, "width": 4.0}
```

指定した要素が含まれるかのみで判断するため、他の要素が存在してもマッチすることは注意してください。
AS パターンとの合わせ技で、下のような型の判定のような記述も可能となります。

```python:値の型を判定
mob = {"name": "ghast", "health": 10, "width": 4.0}

match mob:
    case {"name": str() as name}:
        print(name)

--> ghast
```

`str()` のようなビルトイン関数は下のようにも記述できます。

```python:ビルトイン関数特有の記述
mob = {"name": "ghast", "health": 10, "width": 4.0}

match mob:
    case {"name": str(name)}:
        print(name)

--> ghast
```

```python:上を if で
mob = {"name": "ghast", "health": 10, "width": 4.0}

if "name" in mob and isinstance(mob["name"], str):
    print(mob["name"])
```

シーケンスパターンほどの差はないですが、マッチ文だと意図が明瞭にみえます。

:::note
if 文と `isinstance()` を同時に利用する場合は、ぜひマッチ文も検討してください
:::

### 他の要素をキャプチャ

関数定義のキーワード可変長引数と似ていますが、`**` を記述するだけです。

```python:パターン以外の要素を取得
mob = {"name": "ghast", "health": 10, "width": 4.0}

match mob:
    case {"name": str(name), **features}:
        print(features)

--> {"health": 10, "width": 4.0}
```

## クラスパターン

クラス系にマッチするパターンです。
今まで同様にパターン内の変数に値を束縛します。

```python:単純なクラスのマッチング
class Pokemon:
    def __init__(self, name, type, id):
        self.name = name
        self.type = type
        self.id = id


pikachu = Pokemon("Pikachu", "Electric", 25)

match pikachu:
    case Pokemon(name="Pikachu", type=type, id=id):
        print(type)
        print(id)

--> Electric
    25
```

```python:上を if で
pikachu = Pokemon("Pikachu", "Electric", 25)

if isinstance(pikachu, Pokemon):
    print(pikachu.type)
    print(pikachu.id)

--> Electric
    25
```

やはりマッチ文のほうが直感的かつ明瞭にみえます。

### 位置引数によるマッチング

実はデフォルトだと位置引数によるマッチングができません。
対処法は簡単で、クラス定義に `__match_args__` を追加するだけです。

```python:パターンに位置引数を適用
class Pokemon:
    def __init__(self, name, type, id):
        self.name = name
        self.type = type
        self.id = id

    __match_args__ = ("name", "type", "id")


pikachu = Pokemon("Pikachu", "Electric", 25)

match pikachu:
    case Pokemon("Pikachu", type, id):
        print(type)
        print(id)

--> Electric
    25
```

`__match_args__` に属性名を文字列としてタプルで渡すだけです。

## ガード

`case` の後ろに `if` を記述することで、複雑な条件に柔軟に対応できます。

下ではガラル地方のポケモン (`id` が 810 ~ 898) のみ表示するために、パターンの後に if 文を追加して、制限を設けることでより細かな条件を指定しています。

```python:パターンに条件を追加
class Pokemon:
    def __init__(self, name, type, id):
        self.name = name
        self.type = type
        self.id = id

    __match_args__ = ("name", "type", "id")


pikachu = Pokemon("Pikachu", "Electric", 25)
cinderace = Pokemon("Cinderace", "Fire", 815)


match pikachu:
    case Pokemon(name, type, id) if 810 <= id <= 898:
        print(type)
        print(id)

match cinderace:
    case Pokemon(name, type, id) if 810 <= id <= 898:
        print(type)
        print(id)

--> Fire
    815
```

エースバーン (英名 : Cinderace) は if 文の条件を満足するため、タイプと図鑑 No. (ここでは `id`) が表示されます。

## まとめ

if 文でも記述はできるので、マッチ文に触れたことない方も多かったかもしれません。
とはいえ前述した 10 のパターンとガードさえおさえれば、マッチ文のスペシャリストといっても過言ではありません。
今後条件分岐を記述する際は、マッチ文と if 文を使い分けてロバストなコーディングを目指してください。

# 更新履歴

2025/04/23 初版
2025/04/24 改行を調整 & 誤植を修正