Python の構造的パターンマッチング 1 の使い方についてかんたんにまとめました。
構造的パターンマッチングは一見シンプルですがさまざまな使い方ができる非常に強力な機能です。 正しく理解してうまく活用できれば Pythonic なコードを書くのに役立つものと思います。
目次
構造的パターンマッチングとは
構造的パターンマッチング( Structual Pattern Matching )は、 Python 3.10 で新たに導入された(される)構文です。
2 つのキーワード match
と case
を使用します。
match value:
case 1:
...
case _:
...
単純な値での分岐の他に、データ構造やアトリビュートのパターンでの分岐なども可能となっています。
見た目は他のオブジェクト指向型の言語によくある switch
case
に似た印象を与えますが、どちらかというと関数型言語のパターンマッチングに近いイメージで捉えるのがよさそうに思います。
対応する PEP は 634 635 636 です:
- PEP 634 - Structural Pattern Matching: Specification | peps.python.org
- PEP 635 - Structural Pattern Matching: Motivation and Rationale | peps.python.org
- PEP 636 - Structural Pattern Matching: Tutorial | peps.python.org
「 Python Pattern Matching 」等で検索すると PEP 622 が引っかかることがありますが PEP 622 は PEP 634 に置き換えられた古いものです。
構造的パターンマッチングがサポートするパターン
数多くのパターンをサポートしています。 以下シンプルなものから順にいくつかのパターンを見ていきましょう(すべてのパターンを押さえたい場合は公式ドキュメントをご覧ください)。
- リテラルパターン
- キャプチャパターン
- ワイルドカードパターン
- バリューパターン
- シーケンスパターン
- マッピングパターン
if
ガード- クラスパターン
as
での束縛
リテラルパターン
case
の後ろにリテラルを記述するパターンです。
このパターンでは単純に値が一致するかどうかでマッチングを行います。
number = 10
match number:
case 0:
raise Exception('失敗')
# ここがマッチ
case 10:
print('成功')
case _:
raise Exception('失敗')
リテラルパターンにかぎったことではありませんが、処理内容が同じものは |
( or )でつなげて 1 つの case
文にまとめることができます。
status = 500
match status:
case 200:
raise Exception('失敗')
# ここがマッチ
case 500 | 501 | 502 | 503:
print('成功')
case _:
raise Exception('失敗')
キャプチャパターン
case
の後ろに Python で有効な名前を記述するパターンです。
次のサンプルでは case x:
の部分がそれにあたります。
value = 3.14
match value:
case 3:
raise Exception('失敗')
case 3.1415:
raise Exception('失敗')
# ここがマッチ
# キャプチャパターンは必ず最後に置く必要がある(でないと SyntaxError )
case x:
assert x == 3.14
print('成功')
このように書くと case x:
のところでマッチし、変数 x
の値が 3.14
になります(正確に言うと「名前 x
が値 3.14
に束縛されます」)。
キャプチャパターンは必ずマッチするため、複数ある case
の最後に置く必要があります。
つまり、キャプチャパターンよりも後に他の case
を書くことはできません。
キャプチャパターンよりも後に case
を置こうとすると SyntaxError
が発生します。
ワイルドカードパターン
case
の後ろに _
を記述するパターンです。
次のサンプルでは case _:
の部分がそれにあたります。
message = 'こんにちは'
match message:
case 'おはよう':
raise Exception('失敗')
# ここがマッチ
# ワイルドカードパターンは必ず最後に置く必要がある(でないと SyntaxError )
case _:
# `_` に代入されるわけではない(キャプチャパターンとは異なる点)
assert '_' not in locals()
print('成功')
ワイルドカードパターンはキャプチャパターンとよく似ていますが、次の点が異なります。
- キャプチャパターン: 変数への代入が起こる
- ワイルドカードパターン: 変数への代入が起こらない
そのため、上のサンプルでは _
は locals()
に含まれません。
ワイルドカードパターンもキャプチャパターンと同じく必ずマッチするため、複数ある case
の最後に置く必要があります。
バリューパターン
case
の後ろに .
を 1 つ以上含む有効な名前を記述するパターンです。
このパターンでは、指定された名前の値と一致するかどうかでマッチングが行われます。
class Color:
RED = 'red'
YELLOW = 'yellow'
GREEN = 'green'
color = 'green'
match color:
case Color.RED:
raise Exception('失敗')
case Color.YELLOW:
raise Exception('失敗')
# ここがマッチ
case Color.GREEN:
print('成功')
case _:
raise Exception('失敗')
バリューパターンはパッケージやクラスに含まれる定数でマッチングするためのものとして用意されているようです。
.
を含まない場合はリテラルパターンになってしまうので注意が必要です。
たとえば、次のように書くと case RED:
のところがキャプチャパターンになり、その後に case
が存在するので SyntaxError
が発生します:
# これはシンタックスエラーになる
RED = 'red'
YELLOW = 'yellow'
GREEN = 'green'
color = 'green'
match color:
# `.` を含まないのでバリューパターンではなくキャプチャパターンになってしまう
case RED:
raise Exception('失敗')
case YELLOW:
raise Exception('失敗')
case GREEN:
print('成功')
case _:
raise Exception('失敗')
# => SyntaxError: name capture 'RED' makes remaining patterns unreachable
シーケンスパターン
list
や tuple
などのシーケンス系のデータ型にマッチするパターンです。
シーケンスパターンは ()
か []
を使用します。
item = ['チョコ', 1, True]
match item:
case ['バニラ', 1, True]:
raise Exception('失敗')
# ここがマッチ
case ['チョコ', count, topping]:
assert count == 1
assert topping is True
print('成功')
values = [1, 'Q', 3]
match values:
# ここがマッチ
# `(x, y, z)` と `[x, y, z]` は同等の扱いになる
case (1, 'Q', 3):
print('成功')
case _:
raise Exception('失敗')
(筆者が理解しているかぎり) ()
と []
に本質的な違いはなく、どちらを使っても同じ処理になります。
実際に利用する際はどちらかひとつに統一するのがよいかと思います。
各要素には他のマッチングパターンを使うことができるので、柔軟なマッチングが可能です(おそらく、実践で主に使うのはリテラルパターンとキャプチャパターンになるかと思います)。
マッピングパターン
dict
などのマッピング系のデータ型にマッチするパターンです。
その要素と値でマッチができます。
adict = {
'タコ': 'たこ焼き',
'イカ': 'いか焼き',
}
match adict:
case {'タコ': 'Octopus', 'イカ': 'いか焼き'}:
raise Exception('失敗')
# ここがマッチ
# 長ければ複数行に分けることもできる
case {
'タコ': x,
'イカ': y,
}:
assert x == 'たこ焼き'
assert y == 'いか焼き'
print('成功')
case _:
raise Exception('失敗')
シーケンスパターンと同様、要素には他のパターン(バリューパターンやキャプチャパターン)を使用できます。
if
ガード
各 case
には、その後に if ~
を付けることで詳細の条件を追加することができます。
公式ドキュメントではこれは「ガード」( guard )と呼ばれています。
values = (10, 15)
match values:
# `case ...` の後ろに `if` で条件を追加できる
case x, y if x == y:
raise Exception('失敗')
case x, y if x > y:
raise Exception('失敗')
# ここがマッチ
# 10 < 15 で `if` の条件を満たすのでここにマッチする
case x, y if x < y:
print('成功')
case _:
raise Exception('失敗')
クラスパターン
オブジェクトに対して、クラスとそのアトリビュートでマッチするパターンです。
サンプルの準備として、シンプルな Product
クラスを定義しておきます。
class Product:
sku: str
name: str
def __init__(self, stock_keeping_unit: str, namae: str):
"""アトリビュート名と引数名を意図的に変えている"""
self.sku = stock_keeping_unit
self.name = namae
この Product
クラスを case
で指定することができます。
product = Product('tako', 'たこ焼き')
match product:
# `name` が異なる
case Product(sku='tako', name='タコヤキ'):
raise Exception('失敗')
# `sku` が異なる
case Product(sku='otako', name='たこ焼き'):
raise Exception('失敗')
# ここがマッチ
# `sku` `name` ともに一致
case Product(sku='tako', name='たこ焼き'):
print('成功')
case _:
raise Exception('失敗')
case Product(...):
のところでは一見インスタンスが生成されるようにも見えますが、インスタンスは生成されません。
クラスパターンでは、たとえば
case Product(sku='tako', name='たこ焼き'):
と書くと、インスタンスは生成せず次の 2 つの条件をともに満たす場合のみマッチします。
- マッチ対象が
Product
のインスタンスである - アトリビュートの
sku
の値が'tako'
でname
の値が'たこ焼き'
である
このロジックを従来の if
文で実現しようとすると次のように冗長になりがちですが、クラスパターンを使用すると簡潔に記述できます。
if isinstance(product, Product):
if product.sku == 'tako' and product.name == 'たこ焼き':
...
クラスパターンでは、特殊なクラスアトリビュート __match_args__
を定義することで、 case
におけるアトリビュートの名前の指定を省略することもできます。
class Point:
# `match ~ case` でアトリビュート名を省略できるよう `__match_args__` を定義する
__match_args__ = ("x", "y")
x: int
y: int
def __init__(self, x, y):
self.x = x
self.y = y
point = Point(3, 5)
match point:
# アトリビュートの名前を省略できる
case Point(3, 10):
raise Exception('失敗')
case Point(4, 5):
raise Exception('失敗')
# ここがマッチ
case Point(3, 5):
print('成功')
case _:
raise Exception('失敗')
as
での束縛
キーワード as
を使えば、マッチしたパターンの一部のパーツを変数に代入することができます(正確には「名前を束縛できます」)。
次のサンプルでは、上の「シーケンスパターン」と「クラスパターン」を組み合わせたパターンにおいて、マッチした Point
インスタンスを as
を使って p1
p2
という変数に代入しています。
class Point:
__match_args__ = ("x", "y")
x: int
y: int
def __init__(self, x, y):
self.x = x
self.y = y
points = [Point(3, 5), Point(8, 10)]
match points:
case (Point(x1, y1) as p1, Point(x2, y2) as p2):
assert p1.x == 3
assert p1.y == 5
assert p2.x == 8
assert p2.y == 10
print('成功')
case _:
raise Exception('失敗')
以上です。
複合型の「シーケンスパターン」「マッピングパターン」「クラスパターン」と if
ガードを組み合わせると柔軟な分岐が可能になるので、従来は if
文をいくつも入れ子にして実現する必要があった処理をシンプルに記述できる可能性があります。
ということで、かんたんですが Python の構造的パターンマッチング( match
case
)の使い方のかんたんなまとめでした。
非常に強力なので使いどころがいろいろありそうですが、正しく理解しないまま使うと思わぬバグを生むことにもなりそうです。 実際に利用する前は、公式のドキュメントを読んだり実際にコードを書いてみたりして挙動を正しく理解した上で利用することをおすすめします。
記事内のサンプルに近いものを GitHub に置きました。 興味のある方はご覧ください。
参考
構造的パターンマッチング関連の PEP です。 順に「仕様」「動機と根拠」「チュートリアル」となっています。
- PEP 634 - Structural Pattern Matching: Specification | peps.python.org
- PEP 635 - Structural Pattern Matching: Motivation and Rationale | peps.python.org
- PEP 636 - Structural Pattern Matching: Tutorial | peps.python.org
構造的パターンマッチングが導入された(される) Python 3.10 の What's New です。
- 本記事執筆時点で Python 3.10 は未リリースです。↩