Python 3.10で追加された構造的パターンマッチ(match case
)の使い方について解説します。
Pythonの構造的パターンマッチ
Python 3.10で新しく追加されたものとして「構造的パターンマッチ (Structural Pattern Matching)」という機能があります。
構造的パターンマッチは、その名の通り評価対象がパターンにマッチするかをチェックして、一致した場合の挙動を定義することができます。構造的パターンマッチは、近年の新機能の中で最も議論を起こした複雑な機能であると言われており、この機能は受理されるまで何度も議論されています。
関連する主なPEP文書であるPEP634, 635, 636の文書は以下になります。
構造的パターンマッチには多くのパターンがPEP634で定義されています。本記事では、PEPで定義されているパターンの例を使いつつ、構造的パターンマッチの基本的な使い方を紹介します。
構造的パターンマッチの基本的な使い方 (match case)
構造的パターンマッチでは、match
とcase
というソフトキーワードが導入されており、match
文として使用します。構文は以下のようになります。
match expression: case pattern: ...
expression
の部分にはPythonの式を指定し、pattern
の部分にはマッチさせるパターンを指定します。複数パターンの場合は、複数case
を記載していきます。case
ブロック内の…部分にはパターンに一致した場合のコードを記述します。
match
文は、C言語のswitch
文に似ていますが、後述するようにシーケンスやクラスのインスタンス内部の値との一致を確認できるなど強力な機能として使用できます。
以下は、他のプログラミングでも出てくるような簡単な分岐処理を、match
文で実現した例です。
value = 2 match value: case 1: print("失敗") # ↓ ここがマッチする case 2: print("成功") case _: print("値は不適切です。")
【実行結果】 成功
上記ではvalue=2
なので「case 2:
」の部分がマッチして「成功」と表示されます。もしvalue=1
の場合は「失敗」という表示になります。なお、match case
では、一番最初にマッチしたcase
以外の処理は実行されませんので注意しましょう。
また、”_
“(アンダースコア)は、後述しますがワイルドカードパターン(Wildcard Patterns)で、必ず一致するパターンとして使用できます。上記例では、1や2以外の数値や文字列等の場合には「値は不適切です。」と表示されます。C言語のswitch
文におけるdefault
のようなものです。
上記例は非常に簡単な例として紹介しましたが、Pythonの構造的パターンマッチのパターンの種類としては多くの種類があり、PEP634には「XXX Patterns」というように記載があります。以降では、代表的なパターンについて簡単な例を使って紹介してみたいと思います。
予約語のことをキーワードと言いますが、ソフトキーワードはすべての文脈ではなく一部の文脈でのみのものになります。そのため、ソフトキーワードであるmatch
やcase
は、match
文の文脈以外では、通常の変数や関数名として使用することができます。
構造的パターンマッチの種類
Pythonの構造的パターンマッチの各種パターンについて、例を使って紹介していきます。
リテラルパターン(Literal Patterns)
最も基本的な、case
の後ろにリテラルを記述するパターンをリテラルパターン(Literal Patterns)と言います。リテラルパターンでは、以下のように単純に値が一致するかどうかでマッチングを行います。
def check(data): match data: case 1: print("失敗") case 2: print("成功") case _: print("値は不適切です。") if __name__ == "__main__": value = 1 check(value) print("---") value = 2 check(value) print("---") value = 3 check(value) print("---") value = "test" check(value)
【実行結果】 失敗 --- 成功 --- 値は不適切です。 --- 値は不適切です。
上記例では、1
であれば失敗、2
であれば成功、その他の場合は不適切な値として処理するようになっています。もちろん、if
、elif
、else
を使った分岐でも実装できます。
キャプチャパターン(Capture Patterns)
キャプチャパターン(Capture Patterns)は、case
の後ろの変数に値をキャプチャして処理をするパターンです。
def check(data): match data: case 1: print("失敗") case x: print(f"成功: {x}") # 以下のような書き方をしてしまうと「case 1:」に到達しない # match value: # case x: # print(f"成功: {x}") # case 1: # print("失敗") if __name__ == "__main__": value = 1 check(value) print("---") value = 2 check(value) print("---") value = 3 check(value) print("---") value = "test" check(value)
【実行結果】 失敗 --- 成功: 2 --- 成功: 3 --- 成功: test
上記例では「case 1:
」の条件に一致しなかったdata
を、変数x
にキャプチャしています。そのため、print
文ではx
を使って値を表示できています。
キャプチャパターンで注意するべきことはcase
の順番です。match case
では、一番最初にマッチしたcase
以外の処理は実行されません。
# 以下のような書き方をしてしまうと「case 1:」に到達しない match value: case x: print(f"成功: {x}") case 1: print("失敗")
例でコメントアウトしてある部分のように、case
の記載順番を変えて「case x:
」を先に持ってきてしまうと「case 1:
」に到達しなくなるので注意してください。
ワイルドカードパターン(Wildcard Patterns)
ワイルドカードパターン(Wildcard Patterns)は、case
の後ろに「_
」を記述して、すべてのパターンをとらえるためのパターンです。既にこれまでの例でも、目的のパターンに一致しないものに対する処理を記述するためにワイルドカードパターンを使用しています。
def show(data): match data: case _: print(data) if __name__ == "__main__": value = 1 show(value) print("---") value = "test" show(value)
【実行結果】 1 --- test
上記例は、あまり意味のあるコードではありませんが、数値や文字列でもマッチしていることが分かるかと思います。
キャプチャパターンと似ていますが、違いは値をキャプチャしないことです。Pythonでは、for
文など後々不要な値を表現する際に「_
」を使いますが、それと同様です。
値パターン(Value Patterns)
値パターン(Value Patterns)は、既に定義されている変数や定数の名前にマッチするパターンです。
以下例は、Enumを使った列挙型の値との一致を確認する例になっています。なお、Enumについては「列挙型enumの使い方の基本」でまとめていますので興味があれば参考にしてください。
from enum import Enum, auto class Color(Enum): RED = auto() GREEN = auto() BLUE = auto() YELLOW = auto() def check(target): match target: case Color.RED: print("赤") case Color.GREEN: print("緑") case Color.BLUE: print("青") case _: print("一致はありません") if __name__ == "__main__": color = Color.RED check(color) print("---") color = Color.GREEN check(color) print("---") color = Color.BLUE check(color) print("---") color = Color.YELLOW check(color)
【実行結果】 赤 --- 緑 --- 青 --- 一致はありません
上記例では、列挙体であるColor
クラスの値を使って色の判定をしています。
シーケンスパターン(Sequence Patterns)
シーケンスパターン(Sequence Patterns)は、list
やtuple
といったシーケンス関連にマッチさせるために使用します。
以下の例は、対象の値が3
で割り切れるか、5
で割り切れるかをリストで表現しつつ、一致の確認を行っている例です。
def devisible_by_3_5(data): sequence = [data % 3, data % 5] match sequence: case (0, 0): print(f"{data}: 3と5で割り切れる") case (0, _): print(f"{data}: 3で割り切れる") case (_, 0): print(f"{data}: 5で割り切れる") case _: print(f"{data}") if __name__ == "__main__": for d in range(1, 16): devisible_by_3_5(d)
【実行結果】 1 2 3: 3で割り切れる 4 5: 5で割り切れる 6: 3で割り切れる 7 8 9: 3で割り切れる 10: 5で割り切れる 11 12: 3で割り切れる 13 14 15: 3と5で割り切れる
上記例では、判定の前に「sequence = [data % 3, data % 5]
」で、3
で割った余りと5
で割った余りのリストを作成しています。
case
文では、sequence
が「(0, 0)
」「(0, _)
」「(_, 0)
」「_
」のどの形式にマッチするかを確認しています。「_
」については、ワイルドカードパターンなのでどんな値や型でもよい部分です。
今回対象のsequence
はリストですが、case
では「(0, 0)
」といったタプルのような記載でも一致を確認できます。このようにリストやタプルといったシーケンスの構造に対して一致を確認することが可能です。
マッピングパターン(Mapping Patterns)
マッピングパターン(Mapping Patterns)は、辞書(dict
)等のマッピング関連の一致を確認するために使用します。
以下は、条件を記載している辞書に対してキーで一致させて、値を表示しているような例になっています。
def check(target): match target: case {"cond1": cond1, "cond2": cond2}: print(cond1, cond2) case {"cond3": cond3, **items}: print(cond3) print(items) case {**items}: print(items) case _: print("対象外") if __name__ == "__main__": data = {"cond1": 10, "cond2": 20} check(data) print("---") data = {"cond3": 30, "cond4": 40, "cond5": 50} check(data) print("---") data = {"cond6": 60, "cond7": 70} check(data) print("---") data = [1, 2, 3, 4, 5] check(data)
【実行結果】 10 20 --- 30 {'cond4': 40, 'cond5': 50} --- {'cond6': 60, 'cond7': 70} --- 対象外
上記例では、"cond1"
や"cond2"
といったキーで一致を確認しています。case
でキーが一致した場合には、cond1
やcond2
といった変数に値がキャプチャされます。
「case {"cond3": cond3, **items}:
」という部分では、「**items
」という表現を使っています。これは関数やメソッドでキーワード引数をまとめて受け取る**kwargs
の考え方と同じでその他の項目をキャプチャします。"cond3"
が一致する場合は、cond3
に値をキャプチャしつつ、その他の項目はitems
にキャプチャされます。同様に「case {**items}:
」としている部分では、それよりも上のcase
で一致しなかった辞書が全てitems
にキャプチャされます。
ワイルドカードパターンについては、辞書以外が指定されてきた場合の処理を記載するために使用できます。設定やデータなどが辞書で条件の一致を確認したいとき等に使用すると便利な場合があります。
クラスパターン(Class Patterns)
クラスパターン(Class Patterns)は、クラスのオブジェクトに対して、その属性で一致を確認するパターンです。
以下例では、Point
という2次元の位置を表すデータクラスを用意して、値で一致を確認する例を紹介します。なお、クラス定義では@dataclass
デコレータを使用しています。@dataclass
デコレータについては「dataclassの使い方の基本」でまとめていますので興味があれば参考にしてください。
キーワード引数でパターンマッチ (dataclass)
以下は、Point
クラスのインスタンスの一致をキーワード引数を使ってマッチングする例です。
from dataclasses import dataclass @dataclass class Point: """位置クラス""" x: int y: int def where_is(point): match point: case Point(x=0, y=0): print(f"原点: {point}") case Point(x=0, y=y): print(f"Y={y}: {point}") case Point(x=x, y=0): print(f"X={x}: {point}") case Point(): print(point) case _: print("Pointではない") if __name__ == "__main__": pt1 = Point(0, 0) where_is(pt1) print("---") pt2 = Point(0, 5) where_is(pt2) print("---") pt3 = Point(5, 0) where_is(pt3) print("---") pt4 = Point(10, 10) where_is(pt4) print("---") pt5 = (20, 20) where_is(pt5)
【実行結果】 原点: Point(x=0, y=0) --- Y=5: Point(x=0, y=5) --- X=5: Point(x=5, y=0) --- Point(x=10, y=10) --- Pointではない
上記例では、Point
クラスのインスタンスの内部まで構造の一致を確認していて、色々なことが起こっています。case
の部分を表に整理してみます。
case | 一致する内容 |
---|---|
case Point(x=0, y=0): | Point クラスのインスタンスであり、かつ、x=0 , y=0 である場合。 |
case Point(x=0, y=y): | Point クラスのインスタンスであり、かつ、x=0 である場合。y の値は変数y にキャプチャされる。 |
case Point(x=x, y=0): | Point クラスのインスタンスであり、かつ、y=0 である場合。x の値は変数x にキャプチャされる。 |
case Point(): | Point クラスのインスタンスである場合。 |
case _: | その他で常にマッチするワイルドカードパターン |
クラスパターンでは、上記で紹介したようないくつかのパターンが混ざって動作をしています。インスタンス内の属性の値でマッチをとりつつ、必要に応じて変数に値をキャプチャしています。最後にはワイルドカードパターンも使っています。
Point(x=0, y=0)
といった記載はコンストラクタ呼び出しをイメージする方が多いかと思いますが、case
で指定される場合はコンストラクタが呼び出されることはありません。この辺りは最初、混乱する部分かもしれません。
位置引数でパターンマッチ (dataclass)
上記でキーワード引数の形式で一致を見てきましたが、位置引数の形式で「case Point(0, 0):
」「case Point(0, y):
」「case Point(x, 0):
」というように指定することも可能です。
def where_is(point): match point: case Point(0, 0): print(f"原点: {point}") case Point(0, y): print(f"Y={y}: {point}") case Point(x, 0): print(f"X={x}: {point}") case Point(): print(point) case _: print("Pointではない")
【実行結果】 原点: Point(x=0, y=0) --- Y=5: Point(x=0, y=5) --- X=5: Point(x=5, y=0) --- Point(x=10, y=10) --- Pointではない
ただし、位置引数を使用する場合は注意が必要です。位置引数の形式を使うには、クラスに__match_args__
の定義が必要になります。
上記の@dataclass
デコレータを使った例では、データクラスでは、__match_args__
をデフォルトで自動作成します。そのため、特に意識することなく位置引数の形式で使用できています。
コンストラクタなどを含めて自分で定義する場合には、以下のように__match_args__
を明示的に定義する必要があるので注意しましょう。
class Point: """位置クラス""" # 属性の順序を__match_args__に追加する必要がある __match_args__ = ("x", "y") def __init__(self, x: int, y: int): self.x = x self.y = y def __repr__(self): return f"Point(x={self.x}, y={self.y})"
ASパターン(AS Patterns)
ASパターン(AS Patterns)は、case
で一致した際に、as
で指定した変数に対象を束縛することができるパターンです。
以下は、シーケンスパターンとクラスパターンを組み合わせつつ、変数にクラスのインスタンスを束縛する例となっています。
from dataclasses import dataclass @dataclass class Point: """位置クラス""" x: int y: int def show(points): match points: case (Point() as p1, Point() as p2): print(p1) print(p2) case _: print("対象外") if __name__ == "__main__": target = [Point(0, 0), Point(10, 10)] show(target) print("---") target = Point(20, 20) show(target)
【実行結果】 Point(x=0, y=0) Point(x=10, y=10) --- 対象外
上記例では、「case (Point() as p1, Point() as p2):
」の部分で、Point
クラスのインスタンスを2つ含むリストの一致を確認することができます。一致した場合には、リストの要素をそれぞれp1
, p2
に束縛します。その後のprint
で、p1
やp2
を表示できていることが分かるかと思います。
ORパターン(OR Patterns)
ORパターン(OR Patterns)は、case
の中で複数条件のORで一致をさせることができるパターンで、「|
」で条件をつなげることができます。
分かりやすい一つの例としては、以下のようにHTTPレスポンスのパターンを振り分けるような例かと思います。
def check_status(status): match status: case 200: print("OK") case 500 | 501 | 502: print("サーバーエラー") case _: print("対象外") if __name__ == "__main__": response = 200 check_status(response) print("---") response = 500 check_status(response) print("---") response = 501 check_status(response) print("---") response = 502 check_status(response) print("---") response = 400 check_status(response)
【実行結果】 OK --- サーバーエラー --- サーバーエラー --- サーバーエラー --- 対象外
HTTPレスポンスでは、200はリクエストが成功したことを表します。一方で、500, 501, …といった500番台はサーバーエラーに関するレスポンスです。
サーバーエラーはまとめて同様の処理としたいような場合等には、「case 500 | 501 | 502:
」といったように列挙できます。この例はリテラルパターンとORパターンの組み合わせということになりますが、他のパターンとORパターンを組み合わせももちろん可能です。
まとめ
Python 3.10で追加された構造的パターンマッチ(match case
)の使い方について解説しました。
構造的パターンマッチは、その名の通り評価対象がパターンにマッチするかをチェックして、一致した場合の挙動を定義することができます。PEP634では「XXX Patterns」というようにいくつかのパターンが定義されており、本記事では主要なパターンに関して例を使って紹介しました。
構造的パターンマッチを使わなくてもif
文で条件分岐を書くことはもちろんできますが、構造的パターンマッチのmatch
文をうまく使いこなせると非常に可読性高くコードが書ける可能性があります。
しかしながら、上記で触れたように構造的パターンマッチには多岐にわたる使い方が存在し、比較的複雑な側面があります。そのため、十分な理解なしに使用すると、意図しない挙動やバグを引き起こすリスクも考えられます。使用する際には、システムの要件や状況を十分に検討し、適切に利用することが望ましいです。
より詳細な情報や背景については、構造的パターンマッチの主なPEP文書であるPEP634, 635, 636の文書を参照してください。
構造的パターンマッチの公式ドキュメントはこちらを参照してください。
上記で紹介しているソースコードについてはgithubにて公開しています。参考にしていただければと思います。