【Python】isinstanceをmatch/caseに置き換える

## 概要
ある日、下記のような(めんどくさい)処理を書いていました。
```python
# int型かstr型かfloat型かNoneか、よく分からない何かを受け取る
number: Optional[int | str | float] = GetHoge()
if number is None:
    # Noneのときの処理
    ...
elif isinstance(number, int):
    # int型のときの処理
    ...
elif isinstance(number, str):
    # str型のときの処理
    ...
elif isinstance(number, float):
    # float型のときの処理
    ...
else:
    # ここに来ることは現時点で考えられない
    raise TypeError
```
設計が悪いと言えばそうなんだけど、もっと楽に書けないかな～って調べてたら「構造的パターンマッチング(match/case)」に行きついたので紹介します。  
  
## match/case とは
完全に理解したいのであれば、PEP636を読むことをおすすめします。

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

ドキュメントもありますが、似たようなことを書いてあるのでお好きな方を…  

https://docs.python.org/ja/3/reference/compound_stmts.html#the-match-statement

ここでは基本的な使い方をまとめておきます。

例えば、受け取ったコマンドによって処理を決めたい場合などは下記のように書くことが出来ます。
```python
command = input()
match command:
    case "Run":
        # コマンドとして"Run"を受け取った時の処理。
        Run()
    case "Stop":
        # コマンドとして"Stop"を受け取った時の処理。  
        Stop()
    case _:
        # どのcaseにも一致しなかったときの処理。  
        raise ValueError("そのコマンドは無効です")
```
1. `match command`で、`command`に格納されている値を取得します。  
2. `case`を上から順に見ていき、1で取得した値と一致しているかを判定します。 
3. パターンに一致した場合は、そのcaseブロックに記載された処理を行い、match文を抜けています。
4. どのcaseにも一致していなければ、`case _`ブロックの処理が実行されます[^1]。

C言語などの`switch`に似ていますが、大きな違いは`case`の最後に`break`は不要なところですかね。  

### 型でmatch/case

`match case`を一通り見たとき、最初に示したコードは下記のように書けるんじゃないか？と思いました。
```python
number: Optional[int | str | float] = GetHoge()
match number:
    case int:
        print("Integer!!!")
    case str:
        print("String!!!")
    case float:
        print("float!!!")
    case None:
        print("None!!!")
    case _:
        raise TypeError
```
しかし、これは`Syntax Error`になってしまいます。  
`case`の後の`int`,`str`,`float`は**キャプチャパターン**として扱われてしまうためです([参考](https://peps.python.org/pep-0634/#capture-patterns))  
→ 型とは別の扱いになってしまう[^2]  
  
では、どのように指定するのかと言うと…
```python
number: Optional[int | str | float] = GetHoge()
match number:
    case int():
        print("Integer!!!")
    case str():
        print("String!!!")
    case float():
        print("float!!!")
    case None:
        print("None!!!")
    case _:
        raise TypeError
```
のように書きます。これは**クラスパターン**と呼ばれるものです([参考](https://docs.python.org/ja/3/reference/compound_stmts.html#class-patterns))

## 補足

### Type Guard
嬉しいことに`case`ブロック直下の`number`は、`case`に応じて型チェッカーが働いてくれます。  

- `match`の横の`number`の型
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2716775/53d27664-235f-34ef-9583-7fc1e289362e.png)
- `case int()`ブロック内の`number`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2716775/f5f8966e-6ca3-b27e-1df4-079bb673d8dd.png)

### 自作クラスの場合のクラスパターン
中身の要素が異なるときに、下記のようなクラスパターンを書くとどうなるでしょうか？  
```python
@dataclass
class User:
    name: str = "hoge"

def main():
    my_user = User("fuga")
    match my_user:
        case User():
            print("User")
        case _:
            print("other")
```
答えは`case User`が評価されて、`User`が出力されます。 
これは、クラスパターンの引数が存在しない場合は`isinctance(my_user, User)`と同義だからです[^3]。  

属性の一致も見たいのであれば、`case User()`の引数にチェックする属性を入れてあげましょう。  
```python
def main():
    my_user = User("fuga")
    match my_user:
        case User("hoge"):
            print("User")
        case _:
            print("other")
```
この場合は`other`が出力されます。  

## 終わりに
`match case`、意外と難しいと感じました…(特に構文が厄介)  
でも毎回評価に用いる変数を書かなくていいのはめちゃくちゃ好きです。  

ちなみにPython3.10から追加されたので、使用する際にはバージョンを確認してください。  

[^1]: ワイルドカードと呼ばれるもので、これは必ず一致する。  
[^2]: `None`は「リテラル パターン」として扱われるので、処理上問題ない(`True`/`False`も同じ)  
[^3]: PEP634の`If no arguments are present, the pattern succeeds if the check succeeds. Otherwise:isinstance()`より