Python3.10 新機能！パターンマッチ構文解説


# はじめに
Python3.10.0 が2021年10月4日にリリースされました。その中でもパターンマッチ構文は大きな機能追加の一つだったため、この記事で解説したいと思います。

https://www.python.org/dev/peps/pep-0634/


私が運営しているYouTubeチャンネルの動画内でも解説しているのですが、動画をみるのが苦手、テキストで知りたい、という人向けにQiitaでも記事を投稿することにしました！

https://youtu.be/76g-U7BhEbU

# 基本構文
パターンマッチは、あるオブジェクトに対して、どのパターンにマッチしているかを評価して処理を分岐させる構文になります。

`match`の後ろに、調べたい対象のオブジェクトを指定して、`case`の後ろにパターンを記載します。例えば、ひとつ目の`case`のパターン1に当てはまった場合は、`case`の中の処理が実施されます。パターンは色々な記載ができるため、いくつか解説していきます。

```python3:base.py
match 対象オブジェクト:
    case パターン1:
        パターン1がマッチした時の処理
    case パターン2:
        パターン2がマッチした時の処理
```

(今現在はQiitaがPython3.10に対応してないので、シンタックスハイライトが効いてないですね...)

## サンプル1 (値の一致)
以下のようなアルゴリズムをパターンマッチ構文で記載します。

* xが1の時は「値が1です」と表示
* xが2の時は「値が2です」と表示
* xが1と2以外の時は「値が1,2以外です」と表示


```python:sample1.py
x = 2 # 調べたい変数

match x:
    case 1:
        print('値が1です')
    case 2:
        print('値が2です')
    case _:
        print('値が1,2以外です')
``` 

### 詳細解説
最後のcaseの`_`はワイルドカードパターンで、全ての値を表すパターンになります。なので、最後の `case _:`はif文のelseと同じと考えてOKです。
変数は上から順番にパターンとマッチしているかを評価されて、一番初めにマッチしたパターンのcase内を通ります。

C言語やJavaのSwitch文と同じと言えます。

## サンプル2 (値の一致2)
サンプル1のアルゴリズムを少しだけ変更します。

* xが**0また**は1の時は「値が**0または**1です」と表示
* xが2の時は「値が2です」と表示
* xが0と1と2以外の時は「値が0, 1,2以外です」と表示


```python:sample2.py
x = 2 # 調べたい変数

match x:
    case 0 | 1:
        print('値が0または1です')
    case 2:
        print('値が2です')
    case _:
        print('値が0, 1,2以外です')
``` 

### 詳細解説
複数のパターンを`|`で繋げることで、どちらかのパターンがマッチする、という書き方ができます。(OR条件)

また`case`の後ろのパターンを記載するところに、**式を記載することはできません。**そのため、以下のようなコードはエラーになります。

```python:error.py
match x:
    case 1 + 1: # エラーになる
        print('値が2です')
```


## サンプル3 (辞書)
WebAPIのレスポンスを辞書オブジェクトに変換したシチュエーションを考えてみます。

WebAPIは緯度を取得するもので、北半球の場合は`north lat`というkeyに北緯が設定されていて、南半球の場合は`south lat`というkeyに南緯が設定されているとします。

このような辞書オブジェクトに対して、以下のようなアルゴリズムをパターンマッチ構文で記載します。

* `'status': 200`という要素と、keyが`north lat`の要素を持つ辞書の場合は、「北緯 x 度」と表示
* `'status': 200`という要素と、keyが`south lat`の要素を持つ辞書の場合は、「南緯 x 度」と表示
* それ以外は「データ取得に失敗しました」と表示


```python:dict_sample.py
respons_dict = {'status': 200, 'north lat': 10.938} # 調べる変数

match respons_dict:
    case {'status': 200, 'north lat': x}:
        print(f'北緯{x}度')
    case {'status': 200, 'south lat': x}:
        print(f'南緯{x}度')
    case _:
        print('データ取得に失敗しました')
```

### 詳細解説
caseの後の`'status': 200`の記載で「keyが`status`でvalueが`200`である要素が存在する辞書」というパターンを表しています。

また、`'north lat': x`の記載で「keyが`north lat`である要素が存在する辞書」　というパターンを表しています。この時、valueの部分をxを指定したことで、`notch lat`に対応するvalueの値が`x`に代入されます。そのため、caseの中でxを使った`北緯{x}度`という記載ができます。

(一般的な緯度取得WebAPIなら、南半球の緯度はマイナスで表すのが一般的ですが、あくまで例題として...)

## サンプル4 (リスト)
座標を表すリストのオブジェクトに対して、以下のようなアルゴリズムをパターンマッチ構文で記載します。

* 要素が1つの場合は一次元の座標を表していて、「一次元の座標(x)です」と表示させる
* 要素が2つの場合は二次元の座標を表していて、「二次元の座標(x, y)です」と表示させる
* 原点の場合は「N次元の原点です」と表示させる
* それ以外は「想定外のデータです」と表示させる

```python:list_sample.py

cood_list = [10, -30]　# 調べる変数

match cood_list:
    case [0]:
        print('一次元の原点です')
    case [x]:
        print(f'一次元の座標({x})です')
    case [0, 0]:
        print('二次元の原点です')
    case [x, y]:
        print(f'二次元の座標({x}, {y})です')
    case _:
        print('想定外のデータです')
```

### 詳細解説
リストは要素の数ごとにパターンマッチができます。`[x]`なら、要素が1つのリストと表すことができます。そのため、`[0]`は「`0`という要素を１つだけ持つリスト」というパターンを表します。辞書のパターンの説明と同様に、`[x]`は要素が一つのリストで、その値がxに代入されてcaseの中で使うことができます。
`[0, 0]`と`[x, y]`のパターンも要素が２つになっただけで、基本的に同じです。

## サンプル5 (ガード)
`case`の後ろにパターンを記載したとに`if　条件`を記載することで、「パターンにマッチして、かつifの条件もTrueの場合」にcaseの中の処理を実施させるような書き方ができます。これをガードと言います。

```python3:base.py
match 対象オブジェクト:
    case パターン1 if 条件1:
        パターン1がマッチかつ条件1がTrue時の処理
    case パターン2 if 条件2:
        パターン2がマッチかつ条件2がTrue時の処理
             .
             .
```

サンプル4 (リスト)のアルゴリズムを以下のように変更したものを、ガードを使ったパターンマッチ構文で記載します。

* 要素が1つで**かつ値が0以上**の場合は、「一次元の座標(x)です」と表示させる
* 要素が2つで**かつ値が両方0以上**の場合は、「二次元の座標(x, y)です」と表示させる
* それ以外は「想定外のデータです」と表示させる

```python:list_sample.py
cood_list = [10, -30] # 調べる変数

match cood_list:
    case [x] if x >= 0:
        print(f'一次元の座標({x})です')
    case [x, y] if x >= 0 and y >= 0:
        print(f'二次元の座標({x}, {y})です')
    case _:
        print('想定外のエラーです')
```

### 詳細説明
ifの後に書く条件の部分には、パターンマッチの記載で値を代入した変数をそのまま使用することができます。ガードは、両方が満たされている場合にcaseの中が実施されます(AND 条件)

## サンプル6 (クラス)
対象のオブジェクトが、あるクラスのオブジェクトかどうかを調べるには`クラス名()`でパターンマッチができます。

以下のようなアルゴリズムをパターンマッチ構文で記載します。

* 整数型(intクラスのオブジェクト)の場合は「整数です」と表示
* 文字列型(strクラスのオブジェクト)の場合は「文字列です」と表示
* 自作クラスのUserクラスのオブジェクトの場合は「ユーザです」と表示


```python:class_sample.py
x = User(name='斎藤')

match x:
    case int():
        print('整数です')
    case str():
        print('文字列です')
    case User():
        print('ユーザです')
```

## サンプル7 (データクラス)
データクラスの場合は、オブジェクトのクラスと、オブジェクトがもつ変数(インスタンス変数)　の値もパターンで表すことができます。

データクラスの`Student`クラス・`Teacher`クラスがあったとします。それぞれ名前を格納する`name`というインスタンス変数を持っています。Teacherクラスには、`is_principal`という「校長先生なら`True` / それ以外の先生なら`False`」となるboolのインスタンス変数も持っています。

```python:datacalss_sample.py
from dataclasses import dataclass

@dataclass
class Student:
    name: str


@dataclass
class Teacher:
    name: str
    is_principal: bool # 校長先生フラグ
```

この時、Emailの宛名を以下のアルゴリズムで表記する場合のパターンマッチ構文を記載します。

* 生徒の場合「◯◯さんへ」
* 先生の場合「◯◯先生へ」
* 校長先生の場合「◯◯校長先生へ」

```python:dataclass_sample.py
user = Teacher(name='斎藤', is_principal=False)

match user:
    case Student():
        print(f'{user.name}さんへ')
    case Teacher(is_principal=True):
        print(f'{user.name}校長先生へ')
    case Teacher():
        print(f'{user.name}先生へ')
```

### 詳細説明
`Teacher(is_principal=True)`で`Teacher`クラスのインスタンス変数`is_principal`の値が`True`のオブジェクト、というパターンを表している。

校長先生かそれ以外の先生かを判別するためには、`is_principal`の値を調べる必要があるため、二つ目の`case`に使用した。
この書き方はデータクラスではない普通のクラスで使用する場合、同値の判定の仕方が異なるため、使用が複雑になる(`__eq__`をオーバーライドする必要がある)ことに注意。(基本的にデータクラスでのみ、使用するものと思います。)

# おわり
パターンマッチ構文は、まだ他にも書き方がいくつかありますが、よく使いそうなケースをピックアップして記載しました！この記事を読んだ人のお役に立てたら嬉しいです！

また、私の運営するYouTubeチャンネル**PythonプログラミングVTuberサプー**では、基本的なPythonの構文から最新のPythonの書き方まで、初心者でもわかりやすい動画を色々出しているので、良ければ覗いてみてください！！

https://www.youtube.com/channel/UC5Kgc_HNzx4GJ-w4QMeeKiQ