Help us understand the problem. What is going on with this article?

Python3 に提案されたばかりのパターンマッチング構文を調べてみた

昨夜、突如 PEP 622 (Structural Pattern Matching)python/peps に投稿されました
python 系のメーリングリストを追いかけていない僕にとっては突然の大物 Draft の登場で、大混乱です。

というわけで、今回は 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 と似た書き方とのことです。

また、他の 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 と 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 も今まで通り使えます。

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?)が楽しみです

  1. "Python doesn't have expressive ways of destructuring object data (i.e. separating the content of an object into multiple variables)." よい説明があれば置き換えたい… 

  2. 原文では assignment (代入)と読んでいますが、ここでは理解しやすさを優先してキャプチャと呼ぶことにしました。他の言語だと変数束縛(variable binding)と呼ばれたりもします。 

tk0miya
timedia
創業20周年の技術者集団。 確かな技術力・豊富な実績をもとに、多種多様な案件を手掛けるシステム会社のパイオニア
https://www.timedia.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした