【Python】match文について調べてみた

## はじめに
　Pythonで条件分岐を行う際、`if`、`elif`、`else`を用いますが、match文も存在しますよね。自分でプログラミングを学ぶ中で、このmatchの使いどころが良く分からなかったので、少し調べてみました。

## match文とは
　構造的パターンマッチング(structual pattern matching)を実装するための構文で、Python3.10からある機能らしいです。match文で書けるものは、条件をしっかり指定すればif文でも実装可能ですが、以下のような場合に、簡潔に記述するための機能っぽいです。
```python: if
if isinstance(x, tuple) and len(x) == 2:
   host, port = x
   mode = "http"
elif isinstance(x, tuple) and len(x) == 3:
    host, port, mode = x
# Etc.
```

```python: match
match x:
    case host, port:
        mode = "http"
    case host, port, mode:
        pass
    # Etc.
```

たしかに長さで条件分岐する必要がなく、分かりやすい気がします。

C言語などのswitch-caseにも似ていますが、あちらは条件に合致したcase以降のcaseの処理もすべて行われる（breakしなければ）のに対し、こちらはマッチした部分のみが実行されるなど違いがあります。

## いろいろな書き方
- リテラルパターン（literal_pattern）
- キャプチャーパターン（capture_pattern）
- ワイルドカードパターン（wildcard_pattern）
- バリューパターン（value_pattern）
- グループパターン（group_pattern）
- シーケンスパターン（sequence_pattern）
- マッピングパターン（mapping_pattern）
- クラスパターン（class_pattern）

上記のようにいろいろなマッチングのパターンがあるようなので、一つずつ説明します。

### リテラルパターン
```python
x = 3
match x:
    case 1:
        print('1です')
    case 2:
        print('2です')
    case 3:
        print('3です')
```
一番シンプルな書き方です。単純に上から見ていって、マッチしたcaseの処理が実行されます。

```python
x = 4
match x:
    case 1 as number:
        print('1です')
    case 2 as number:
        print('2です')
    case 3 | 4 as number:
        print('3か4です')
print(number)
```
`as`を使うことで、マッチしたときの値を変数に代入することもできます。また、`|`(or)を使用することで、同じ処理をしたいものを並べられます。（リテラルパターンに限らず使えます）

ちなみに一致するかどうかの判定ですが、数値や文字列は`==`を、`True`、 `False`、 `None`などのシングルトンは`is`を用いているようです。



### キャプチャーパターン
```python
x = 3
match x:
    case 1:
        print('1です')
    case 2:
        print('2です')
    case n:
        print(f'1でも2でもなく{n}です')
```
一番下の`case n`がキャプチャーパターンです。この場合には必ずマッチし、caseの後に宣言された変数へ、値が代入されるようです。キャプチャーパターンはリテラルパターンより後に記述しないと、以下のエラーが出ます。
```
SyntaxError: name capture 'n' makes remaining patterns unreachable
```

```python
x = 8
match x:
    case 1:
        print('1です')
    case 2:
        print('2です')
    case n if n % 2 == 0:
        print(f'3以上の偶数です')
```

必ずマッチさせたくない場合には、ifによる条件（ガード）を付けることができます。（キャプチャーパターンに限らず使えます）

```python
x = (1, 2, 3)
match x:
    case a, b:
        print(a, b)
    case a, b, c
        print(a, b, c)
```
後述するシーケンスパターンに当たりますが、カンマ区切りで同時に複数の値を判定可能です。また、`_`や`*`を使って、以下のようなこともできます。

```python
x = (1, 2, 3, 4, 5)
match x:
    case a, b:
        print(a, b)
    case a, *b, _, c:
        print(a, b, c)    #1 [2, 3] 5
```

ただし`_`には本当に何も代入されません。通常は`print(_)`としても値が出力されますが、この場合は定義されていないのでエラーになります。

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

```python
x = 3
match x:
    case 1:
        print('1です')
    case 2:
        print('2です')
    case _:
        print('1でも2でもありません')
```

キャプチャーパターンとほぼ同じですが、変数名を`_`にすると、何も受けとらず、処理のみ行えます。したがって、if文でいうところの`else`に当たる使い方ができます。他言語のswitch文にある`default`ともよく似ています。

```python
x = 3
match x:
    case 1:
        print('1です')
    case 2:
        print('2です')
    case _ as number:
        print(f'1でも2でもなく{number}です')
```
`as`と組み合わせることで、どのパターンにもマッチしなかった場合に、何の値が渡されたのか簡単に取得できるのは結構便利そうです。

### バリューパターン
```python
class MyClass:
    num1 = 1
    num2 = 2
    num3 = 3
x = 2
match x:
    case MyClass.num1:
        print(f'{MyClass.num1}です')
    case MyClass.num2:
        print(f'{MyClass.num2}です')
    case MyClass.num3:
        print(f'{MyClass.num3}です')
```
書き方としてはキャプチャーパターンと同じなのですが、ドットアクセスする変数名を置いておくと、代入ではなくマッチするか判定してくれます。つまり、リテラルパターンのように動いてくれます。

### グループパターン

```python
x = 3
match x:
    case (1 | 2):
        print('1か2です')
    case (3 | 4):
        print('3か4です')
    case (5 | 6):
        print('5か6です')
```

わざわざ項目を作るか迷ったのですが、カッコで囲むだけです。効果も見やすくなるだけです。公式にも、意図するグループを強調するとしか書かれていません。

### シーケンスパターン
```python
x = (1, 3)
match x:
    case (1, 1):
        print('1, 1です')
    case (1, 2):
        print('1, 2です')
    case (1, 3):
        print('1, 3です')
```
上の例はリテラルパターンのリストやタプル版といった感じの使い方です。ただし、`'`と`"`のように`(`と`[`を区別はしないみたいです。また、カッコ無しでも動きました。

```python
x = (1, 3)
match x:
    case (1, n):
        print(f'1, {n}です')
    case (2, n):
        print(f'2, {n}です')
    case (3, n):
        print(f'3, {n}です')
```
上記のようにキャプチャーパターンと組み合わせられます。これはすごく面白そうな感じがします。

```python
x = list('abcdefg')
match x:
    case ("a", *_, "g"):
        print("aから始まり、gで終わります")
    case ("a", *_):
        print("aから始まります")
    case ("b", *_):
        print("bから始まります")
    case _:
        print("その他から始まります")
```

上手く組み合わせると、こんな感じでちょっとテクニカルな判定もできます。

### マッピングパターン

```python
x = {'バナナ': 'banana', 'ブドウ': 'grape', 'レモン': 'lemon'}
match x:
    case {'リンゴ': 'apple', 'ブドウ': 'grape'} as fruits:
        print(fruits)
    case {'バナナ': 'banana', 'ブドウ': 'grape'}as fruits:
        print(fruits)
    case {'レモン': 'lemon', 'ブドウ': 'grape'}as fruits:
        print(fruits)
```

リテラルパターンの辞書版といった感じです。keyとvalueのどちらも一致している必要がありますが、余計な要素がついていても一致している判定になります。実際、上の例でも2つ目のcaseが処理されます。逆に足りない要素がある場合は、一致していない判定になります。つまり、部分的にでも一致している個所があれば、一致している判定になると言えます。

```python
x = {'バナナ': 3, 'ブドウ': 6}
match x:
    case {'リンゴ': apple, 'ブドウ': grape}:
        print(f'リンゴ{apple}個、ブドウ{grape}個')
    case {'バナナ': banana, 'ブドウ': grape}:
        print(f'バナナ{banana}個、ブドウ{grape}個')
    case {'レモン': lemon, 'ブドウ': grape}:
        print(f'レモン{lemon}個、ブドウ{grape}個')
```
キーの方は変数を指定できませんが、バリューには変数を指定できるので、キャプチャーパターンも使えます。

```python
data = {"type": "circle", "radius": 5}

match data:
    case {"type": "circle", "radius": r} if r > 0:
        print(f"円の半径は {r} です")
    case {"type": "rectangle", "width": w, "height": h}:
        print(f"長方形の幅は {w}、高さは {h} です")
    case _:
        print("未知の形状です")
```

AIが分かりやすいものを書いてくれたので、上手く組み合わせた使用例を置いておきます

### クラスパターン
```python
class Person:
    def __init__(self, name, age, job):
        self.name = name
        self.age = age
        self.job = job

x = Person("たかし", 30, "無職")
match x:
    case Person(name = name, age = age, job = "無職"):
        print(f"名前: {name}, 年齢: {age}")
```
もしくは
```python
class Person:
    __match_args__ = ("name", "age", "job")
    def __init__(self, name, age, job):
        self.name = name
        self.age = age
        self.job = job

x = Person("たかし", 30, "無職")
match x:
    case Person(name, age, "無職"):
        print(f"名前: {name}, 年齢: {age}")
```
このように書くことができます。インスタンスをパターンマッチングさせるのに使います。インスタンスがそのクラスのものであるか、特定の属性を持つかによって、一致するかを判定してくれます。if文で`isinstance`を使って判定している部分を置き替えるのがよさそうです。

ちなみにですが、`case Person(name, age, "無職")`の行は、新しいインスタンスが作成されていそうな見た目をしていますが、そういうわけではないみたいです。`Person`の`__new__`を以下のようにすると確認できます。

```python
class Person:
    __match_args__ = ("name", "age", "job")
    
    @classmethod
    def __new__(cls, *args, **kwargs):
        print('新しいインスタンスが作成されました！')
        return super().__new__(cls)

    def __init__(self, name, age, job):
        self.name = name
        self.age = age
        self.job = job

x = Person("たかし", 30, "無職")
match x:
    case Person("たかし", 25, "無職"):
        print('名前: たかし, 年齢: 25')
    case Person(name, age, "無職"):
        print(f"名前: {name}, 年齢: {age}")
```


### おまけ：namedtuple
　`namedtuple`の場合、クラスパターンとシーケンスパターンの両方にマッチします。以下の例の順番を入れ替えて、試していただければ確認できます。

```python
from collections import namedtuple

Person = namedtuple("Person", ["name", "age", "job"])
x = Person("たかし", 30, "無職")
match x:
    case Person(name, age, "無職"):
        print(f"名前: {name}, 年齢: {age}:クラスパターンにマッチしました")
    case (name, age, "無職"):
        print(f"名前: {name}, 年齢: {age}:シーケンスパターンにマッチしました")
```
また、`namedtuple`の場合は、`__match_args__`が自動設定されるようです。

### おまけ：自作のシングルトン

流石に自作のシングルトンは`==`で比較されるようでした。

```python
class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __bool__(self):
        return False

    def __eq__(self, other):
        return not bool(other)

singleton = Singleton()
print(singleton == None)    # True
print(singleton is None)    # False

x = None

match x:
    case Singleton._instance:  # `is` による比較が行われないのでこっち
        print("Matched Singleton instance")
    case _:
        print("Not Matched Singleton instance")

x = singleton

match x:
    case None:
        print('Matched None')
    case _:    # `is`による比較が行われるのでこっち
        print("Not Matched None")
```

## ちょっと使ってみた

### 特定の単語を抜き出す
```python
word_list = [
    "forest", "cave", "mountain", "desert", "ocean", "river", "lake", "swamp",
    "valley", "hill", "plain", "island", "cliff", "canyon", "gorge", "bay",
    "peninsula", "plateau", "tundra", "savanna"
]
for w in word_list: 
    match list(w):
        case [*_, 'e', _]:
            print(w)
```
別にこのくらいならif文でもできますが、似たような感じで、特定の単語で終わる文を抜き出すような場合など、何かしら応用はできそうです。

### 式を受け取って計算
```python
def calculator(expression):
    match expression.split():
        case [a, "+", b]:
            return float(a) + float(b)
        case [a, "-", b]:
            return float(a) - float(b)
        case [a, "*", b]:
            return float(a) * float(b)
        case [a, "/", b] if b != "0":
            return float(a) / float(b)
        case _:
            return "無効な式です"

print(calculator("3.5 + 5"))
print(calculator("10 - 2"))
print(calculator("4 * 7"))
print(calculator("8 / 0"))
```
これもわざわざmatch文でやることではない気がしますが、何か似たような形で活用できそうです。

### 山札からトランプを引く
```python
import random
card_list = []
for suit in ['ハート', 'スペード', 'ダイヤ', 'クラブ']:
    for number in ['A', 2, 3, 4, 5, 6, 7, 8, 9 ,10, 'J', 'Q', 'K']:
        card_list.append([suit, number])
card_list.append(['Joker'])
x = random.choice(card_list)
match x:
    case [suit, number]:
        print(f"{suit}の{number}が出ました")
    case _:
        print("ジョーカーが出ました")
```
これは結構すっきりかけた印象ですね。

## まとめ
- 条件が複雑で、ネストが深くなりやすい場合
- パターンが明確で一目で処理内容を理解したい場合

このような場合には、if文の代わりにmatch文を用いることで、より簡潔に記述できる時がある。 

## 最後に
　ここまで読んで下さりありがとうございました。意外と機能が多くて調べるのがちょっと大変でしたが、結構使える場面がありそうだったので、どこかで役立てようと思います。

## 参考

https://www.lifewithpython.com/2021/06/python-structual-pattern-matching.html

https://peps.python.org/pep-0634/#value-patterns

https://peps.python.org/pep-0635/

https://peps.python.org/pep-0635/