追記: この記事から8ヶ月後に、パターンマッチ構文は Python に採用されることが決まりました。 PEP 634, 635, 636 (Structural Pattern Matching) を読んだよメモ では最新情報をもとに記事を再構成しています。
昨夜、突如 PEP 622 (Structural Pattern Matching) が python/peps に投稿されました。
python 系のメーリングリストを追いかけていない僕にとっては突然の大物 Draft の登場で、大混乱です。
野心的な PEP が出てきた。なにこれやばい。あとでじっくり読む。 / PEP 622 -- Structural Pattern Matching | https://t.co/HIipocP5Vt https://t.co/o0OUN6BqgH
— tk0miya (@tk0miya) June 23, 2020
というわけで、今回は PEP 622 を読んでみようと思います。
なお、昨日投稿されたばかりのできたてほやほやで、まだ Draft ステータスですから採択されるかどうかはわかりませんし、もし採択されたとしてもどういう形になっているかはわかりませんので、与太話として読んでいただくのが良いかと思います。
(注: PEP 622 のうち、使い方に関する部分についてまとめたものです。実装に近い部分、静的解析などに関する部分はここでは扱いません)
概要
- Python のコードベースを調査したところ、
isinstance()
がよく使われていることがわかった (len()
に続いて 2位) - 多くのケースでは、その直後に属性アクセスが行われていた
- これは多くのプログラムでは、heterogeneous data (ある変数が複数のデータ型を取りうること)が登場することを示している
- また、Python にはオブジェクトを分解してデータを取り出すよい方法がない 1
- Python にパターンマッチング構文を追加し、heterogeneous dataを扱いやすくする
アプローチ
新しいパターンマッチング構文は match と case というふたつのキーワードを用います。
match shape:
case Point(x, y):
...
case Rectangle(x, y, _, _):
...
match
に指定した値が、各 case
句のいずれかにマッチするかどうかを順にチェックしていきます。
後述しますが、else 句はありません。
case 句に指定できる条件には、以下のものがあります。
- Literal Pattern
- Name Pattern
- Constant Value Pattern
- Sequence Pattern
- Mapping Pattern
- Class Pattern
ここでは例とともに各パターンを紹介します。
Literal Pattern
リテラル値を使ったパターンです。文字列、数値、boolean、None が使えます (ただし f-string は除く)。
match number:
case 0:
print("Nothing")
case 1:
print("Just one")
case 2:
print("A couple")
case -1:
print("One less than nothing")
case 1-1j:
print("Good luck with that...")
(Python にはありませんが) switch 文みたいなものですね。
Name Pattern
case 句に変数名を指定すると、常にマッチは成功し、同時に値をキャプチャします2。
以下の例では、変数 greeting
の値が name
に代入されます。
match greeting:
case name:
print(f"Hi {name}!")
Name Pattern は常にマッチに成功してしまうため、条件分岐には役立ちません。
他のパターンと組み合わせることで役立ちます。
なお、同じ case 句の中で同じ変数名を二度以上使うことはできません。
match data:
case [x, x]: # Error!
...
Name Pattern (亜種)
case 句に _
を指定した場合、Name Pattern と同様に常にマッチに成功します。
しかし、値のキャプチャは行われません。
match greeting:
case _:
print(f"Hi {_}!") # Matched, but error because not captured!
この _
という記号は Rust や Scala と似た書き方とのことです。
Rustのmatchも_で何にでもマッチするのでそれに近い感じですかね。https://t.co/WxcGESxh0J
— Hiroaki Nakamura ⋈ (@hnakamur2) June 24, 2020
また、他の Name Pattern の変数と異なり、_
は同じ case 句の中で複数回指定することができます。
match data:
case [_, _]:
print("Some pair")
case の並びの最後に case _:
を置くと else っぽく使える、という説明がありました。
Constant Value Pattern
定数や Enum を使ったパターンです。
case 句にドット付きの名前を渡した場合は Name Pattern ではなく Constant Value Pattern とみなされ、値を使った比較が行われます。
グローバルレベルの変数をパターンとして指定する場合は先頭にドットを付けます (ドットを付けないと Named Pattern になってしまいます)。
from enum import Enum
class Color(Enum):
BLACK = 1
RED = 2
BLACK = 1
RED = 2
match color:
case .BLACK:
print("Black suits every color")
case Color.BLACK:
print("Black suits every color")
case BLACK: # This will just assign a new value to BLACK.
...
Sequence Pattern
シーケンス系のデータ(collections.abc.Sequence
のサブクラス 2)にマッチするパターンです。
list もしくは tuple の記号を使って書きます。
match collection:
case [1, 2, 3]:
print("Got 1, 2, 3")
case (4, 5, 6):
print("Got 4, 5, 6")
これに先ほどの Name Pattern を組みわせると、複雑なデータ型にマッチするパターンを記述できます。
最初のパターンは 2要素のシーケンスで、先頭が 1、次が任意の値にマッチします(そして2番目の要素は変数 x
にキャプチャされます)。
二番目のパターンは 2要素のシーケンスで、先頭が 1、次が 1件以上のデータを持ったシーケンスにマッチします(そして、先頭の要素は x
に、後続の要素は others
にそれぞれキャプチャされます)。
match collection:
case (1, x):
print(f"Got 1 and {x}")
case 1, [x, *others]:
print("Got 1 and a nested sequence")
応用例として以下が挙げられています。
-
[*_]
... 任意の長さのシーケンス (0件以上) -
(_, _, *_)
... 2件以上の長さのシーケンス -
["a", *_, "z"]
... 先頭が"a"
、末尾が"z"
である、任意の長さのシーケンス
Mapping Pattern
マッピング系のデータ(collections.abc. Mapping
のサブクラス)にマッチするパターンです。
dict の記号を使って書きます。
最初のパターンは "route"
というキーを持つマッピングにマッチします (そして値は変数 route
にキャプチャされます)。
二番目のパターンは constants.DEFAULT_PORT
というキーを持つマッピングにマッチします (そして値は sub_config
に、残りのデータは rest
にキャプチャされます)。
import constants
match config:
case {"route": route}:
process_route(route)
case {constants.DEFAULT_PORT: sub_config, **rest}:
process_config(sub_config, rest)
なお、このパターンでは 指定したキーがマッピングに含まれるかどうか だけで判定を行います。
言い換えるとその他のキーが含まれていても、条件にはマッチします。
例えば {"route": 1, "key": 2}
というデータは最初のパターンにマッチします。
なお、 __missing__
や __getitem__
などによってデータを生成するタイプのデータ構造には対応しないため、defaultdict などはマッチしない可能性があります。
Class Pattern
クラス系のデータにマッチするパターンです。
指定したクラスのオブジェクトであることを判定し、属性値が一致するオブジェクトにマッチします。
最初のパターンは Point
クラスのオブジェクトにマッチします (そして、属性はそれぞれ x
, y
にキャプチャされます)。
二番目のパターンは Rectangle
クラスのオブジェクトにマッチします (以下省略)。
match shape:
case Point(x, y):
...
case Rectangle(x0, y0, x1, y1, painted=True):
...
その他
パターンの組み合わせ
|
を使うことで、複数の条件を併記できます。
match something:
case 0 | 1 | 2:
print("Small number")
case [] | [_]:
print("A short sequence")
case str() | bytes():
print("Something string-like")
case _:
print("Something else")
なお、併記されたパターンで Name Pattern を利用する場合、各パターンで同じ変数名を使わなくてはなりません。
match something:
case 1 | x: # Error!
...
case x | 1: # Error!
...
case one := [1] | two := [2]: # Error!
...
case Foo(arg=x) | Bar(arg=x): # Valid, both arms bind 'x'
...
case [x] | x: # Valid, both arms bind 'x'
...
ガード条件
case 句の後ろに if 文を加えることでガード条件を加えることができます。
Name Pattern による変数のキャプチャを付け加えることで、特定のデータ構造に一致するもので、特定の条件を満たす場合にマッチするというパターンが記述できます。
match input:
case [x, y] if x > MAX_INT and y > MAX_INT:
print("Got a pair of large numbers")
case x if x > MAX_INT:
print("Got a large number")
case [x, y] if x == y:
print("Got equal items")
case _:
print("Not an outstanding input")
Named sub-patterns
Walrus operator を使うことでサブパターンを表現できます。
match get_shape():
case Line(start := Point(x, y), end) if start == end:
print(f"Zero length line at {x}, {y}")
match と case は soft keywords 扱い
match ってキーワード、いろんなプログラムとぶつかってギャッとなりそう。大丈夫なんかな。
— tk0miya (@tk0miya) June 23, 2020
最初、match と case をキーワードにするのかと読み違ってぎょっとしましたが、勘違いでした。
The match and case keywords are proposed to be soft keywords, so that they are recognized as keywords at the beginning of a match statement or case block respectively, but are allowed to be used in other places as variable or argument names.
これまでどおり変数や引数などに利用できます。re.match
も今まで通り使えます。
パーサーに詳しくないので推測ですが、多分パーサーが新しくなった影響でしょうね。
— Inada Naoki (@methane) June 24, 2020
今までは新しいキーワードを追加したら(多少の時間差はあれど)予約語になって変数名とか関数名に使えなくなってました。それが async であちこちに迷惑かける事になってしまった。
3.9 から導入された PEG Parser のおかげのようです (PEP 622 の冒頭にも一言触れられてる)。
__match__()
プロトコルについて
Class Pattern の実現には __match__()
、 __match_args__
という特殊メソッド、特殊属性を使うとのことです。
…が、説明を読んでもぱっと理解できなかったので、TODO となっているサンプルの登場を待つことにします。
自分で作成したクラスをマッチングさせるときに、特殊な対応が必要な場合に役立つかもしれません。
感想
- Erlang や Ruby の "あの" パターンマッチングが登場するのか、という驚きがあった
- JSON を始めとする複雑なデータ構造を、シンプルでパワフルな記述でマッピング & 分解できるとしたら本当に素晴らしい
- Ruby 2.7.0 のリリースノート に載っているサンプルを見ると、パワフルさが伝わるのではないかと思います
- これをパターンマッチなしで書くのは結構面倒ですよね
- cpython 向けの実装はおおよそできているとのことなので、早く試してみたい (記事は書いたものの動かせていない)
- まだ Draft 段階で、採用は決まっていないのだけど 3.10 (3.11?)が楽しみです