29
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

PEP 634, 635, 636 (Structural Pattern Matching) を読んだよメモ

今週の火曜日、PEP 634〜636 の 3本の PEP が Accept されました。

この 3本の PEP は、去年の 6月に Python3 に提案されたばかりのパターンマッチング構文を調べてみた として紹介した PEP 622 の後継となる提案です。
今回は約 7ヶ月の議論を経て、Python 3.10 に採用されることが決定したこの Structural Pattern Matching について見ていきましょう。

3本立て

Structural Pattern Matching は PEP 634, 635, 636 の 3本の PEP で構成されています。

それぞれ仕様(PEP 634)、動機・根拠(PEP 635)、チュートリアル(PEP 636)を説明しています。パターンマッチを把握する場合は PEP 636 のチュートリアルから読み進めていくと良いでしょう。
かんたんなゲームを作ってみよう、というストーリー仕立てになっており、シンプルなパターンマッチのサンプルから始まるので読みやすい PEP です。かんたんな英語なので、翻訳ツール片手に読んでみるとよいでしょう。

概要

  • 多くの言語で実装されているパターンマッチを Python にも導入します
  • これまでは isinstance()key in obj などの判定を繰り返す必要があった、データ構造や型による分岐がシンプルな構文で記述できる
  • Python 向けにパターンマッチをアレンジしており、組み込み型を扱うのに必要な柔軟性を備えている
  • 3.10 での採用が決定した
  • パターンマッチは、指定した値が特定のパターンに一致するかを判定する条件分岐の機能と、一致したパターンの一部 (または全部) を変数として抽出するバインドの機能から構成される

アプローチ

新しいパターンマッチング構文は matchcase というふたつのキーワードを用います 1

match shape:
    case Point(x, y):
        ...
    case Rectangle(x, y, _, _):
        ...

match 節に指定した値が、各 case 節で指定したパターンのいずれかにマッチするかどうかを上から順にチェックしていきます。
パターンにマッチした場合は case 節以下のブロックが実行されます。

case 節に指定できるパターンには、以下のものがあります。

  • AS Patterns
  • OR Patterns
  • Literal Patterns
  • Capture Patterns
  • Wildcard Patterns
  • Value Patterns
  • Group Patterns
  • Sequence Patterns
  • Mapping Patterns
  • Class Patterns

ここでは例とともに各パターンを紹介します。

Literal Patterns

リテラル値を使ったパターンです。文字列、数値、複素数、boolean、None が使えます。

match number:
    case 0:
        print("Nothing")
    case 1:
        print("Just one")
    case 2:
        print("A couple")
    case -1:
        print("One less than nothing")
    case True:
        print("Good luck with that...")

他の言語における switch 文みたいなものですね。
値の比較は基本的に == 演算子で行われます 2。そのため、 case 1:case 1.0: は同じものとして扱われます。

Capture Patterns

case 節に変数名を指定すると、常にマッチは成功します。また、マッチと同時に値を変数名に バインド (bind)します。
「バインド」は「代入 (assignment)」とよく似た操作で、変数に対して値が設定されます。
一般的な代入構文(x = 1)では左辺に添字(x[0])や属性(x.attr)を指定できますが、パターンマッチにおけるバインドではこれらは利用できず、変数に対してのみ値を設定することができます。

以下の例では、変数 greeting の値が name にバインドされます。

match greeting:
    case name:
        print(f"Hi {name}!")

Capture Patterns は後述する他のパターンと組み合わせることで、データ構造の一部の値をバインドし、のちのステップで扱うことができます。
たとえば、以下のコードでは command という入力文字列を分解し、後続の処理で利用しています。

match command.split():
    case ["go", direction]:
        current_room = current_room.neighbor(direction)

また、Capture Patterns の変数名に _ は利用できません。 _ を指定した場合は、次の Wildcard Patterns と解釈されます。

Wildcard Patterns

case 句に _ を指定した場合、常にマッチに成功します。Wildcard Patterns では値のバインドは行われません。

match greeting:
    case _:
        print(f"Hi {_}!")  # Matched, but error because not captured!

Wildcard Patterns はすべてのオブジェクトにマッチするため、case 節の並びの最後に配置することで else 代わりに使うことができます。

Value Patterns

クラス定数や Enum など、オブジェクトの属性と値を比較するパターンです。
case 句にドット付きの名前を渡した場合は Capture Pattern ではなく Value Pattern とみなされ、値を使った比較が行われます。

from enum import Enum

class Color(Enum):
    BLACK = 1
    RED = 2

match color:
    case Color.BLACK:
        print("Black suits every color")
    case Color.RED:
        print("Red suits every color")

Group Patterns

パターンを記述する際に、丸カッコを使ってパターンをグルーピングします。

match color:
    case ("BLACK" | 1):
        print("Black suits every color")
    case ("RED" | 2):
        print("Red suits every color")

後に出てくる OR Patterns を使うときなど、読みやすさを増すために使うと良さそうです。

Sequence Patterns

シーケンス系のデータ(collections.abc.Sequence3)にマッチするパターンです。
list もしくは tuple の記号を使って記述します。

match collection:
    case [1, 2, 3]:
        print("Got 1, 2, 3")
    case (4, 5, 6):
        print("Got 4, 5, 6")

これに先ほどの Capture Patterns を組みわせると、複雑なデータ型にマッチするパターンを記述できます。

match collection:
    case (1, x):
        print(f"Got 1 and {x}")
    case 1, [x, *others]:
        print("Got 1 and a nested sequence")
応用例として以下が挙げられています。

最初のパターンは 2要素のシーケンスで、先頭が 1、次が任意の値にマッチします(そして2番目の要素は変数 x にキャプチャされます)。
二番目のパターンは 2要素のシーケンスで、先頭が 1、次が 1件以上のデータを持ったシーケンスにマッチします(そして、先頭の要素は x に、後続の要素は others にそれぞれキャプチャされます)。

なお、ひとつのパターンのなかで同じ Capture Patterns の変数名を 2回以上指定することはできません。たとえば、次のパターンはエラーとなります。

match greeting:
    case (x, x):
        ...

一方、Wildcard Patterns (_) は同じパターンの中で何度でも指定することができます。

match greeting:
    case (_, _):
        ...

また、 Wildcard Patterns と組み合わせることもできます。

  • [*_] ... 任意の長さのシーケンス (0件以上)
  • (, _, *) ... 2件以上の長さのシーケンス
  • ["a", *_, "z"] ... 先頭が "a"、末尾が "z" である、任意の長さのシーケンス

Mapping Patterns

マッピング系のデータ(collections.abc.Mapping 類)にマッチするパターンです。
dict の記号を使って記述します。

import constants

match config:
    case {"route": route}:
        process_route(route)
    case {constants.DEFAULT_PORT: sub_config, **rest}:
        process_config(sub_config, rest)

最初のパターンは "route" というキーを持つマッピングにマッチします (そして値は変数 route にキャプチャされます)。
二番目のパターンは constants.DEFAULT_PORT というキーを持つマッピングにマッチします (そして値は sub_config に、残りのデータは rest にキャプチャされます)。

なお、このパターンでは 指定したキーがマッピングに含まれるかどうか だけで判定を行います。
言い換えると、その他のキーが含まれていても条件にはマッチします。
例えば {"route": 1, "key": 2} というデータは最初のパターンにマッチします。

なお、 Mapping Patterns は obj.get(key, None) メソッドを使ってデータの比較を行います。
そのため、defaultdict などの動的にデータを生成する辞書クラスでは、データの登録状態によってパターンにマッチしないことがあります。

Class Patterns

クラス系のデータにマッチするパターンです。
指定したクラスのインスタンスであることを判定し、属性値が一致するオブジェクトにマッチします。

match shape:
    case Point(x, y):
        ...
    case Rectangle(x0, y0, x1, y1, painted=True):
        ...
    case Rectangle():
        ...

最初のパターンは Point クラスのオブジェクトにマッチします (そして、属性はそれぞれ x, y にキャプチャされます)。
二番目のパターンは Rectangle クラスのオブジェクトで、 painted=True のオブジェクトにマッチします (そして、属性をキャプチャします)。
三番目のパターンは Rectangle クラスのオブジェクトすべてにマッチします。

属性値が一致するかどうかは、 __match_args__ という属性を使って判定されます。詳しくは PEP 634 をチェックしてみてください。Python が提供するビルトイン型、dataclass および namedtuple を利用する場合は自動的に判定されます。

AS Patterns

パターンの中で as を使うことで、パターンの構成要素 (一部) を変数にバインドできます。

match tokens:
    case [left, ('+' | '-') as op, right]:
        ...

OR Patterns

| を使って複数のパターンを併記します。

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 節ではパターンの他に ガード条件 を指定できます。ガード条件は case 句の後ろに if 文で記述します。
ガード条件を指定すると、パターンだけでは表現しづらいマッチ条件を表現できます。

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")

Capture Pattern と組み合わせることで、特定のデータ構造に一致し、なおかつ特定の条件を満たす場合にマッチするというパターンが記述できます。

PEP 622 から削除されたもの

  • 特殊メソッド __match__()
  • セイウチ演算子(:=)によるパターン内部での代入 (AS Patterns に置き換わった)
  • 先頭にドットを付けてグローバル変数をパターンとして表現するやり方 (case .CONSTANT:)
  • Literal Patterns で複素数リテラルの指定方法が変わった (case 1 + 1j:case 1 + 1:)

感想

  • ベースは Python3 に提案されたばかりのパターンマッチング構文を調べてみた でまとめたものと大きく変わりません。
  • しかし、採用に至るまでの議論でパターンの種類が整理されたり、細部が見直されるなど、ブラッシュアップされている印象があります。
  • 当初感じた「シンプルでパワフルな記述でマッピング & 分解できるとしたら本当に素晴らしい」という感想は今も変わりません。
  • 3.10 への採用が決まったので、はやくも秋のリリースが待ち遠しくなりました。
  • また、今回採用を見送ったという custom pattern object がどうなるかも期待したいと思います。
  • 現時点では python/cpython にはマージされていないようです。

  1. matchcase はそれぞれ match 構文の中でのみ利用できるキーワード (ソフトキーワード)です。その他の場面ではキーワードとは扱われないため、これまでどおり変数名に使うことができます。 

  2. 厳密には True, False, None の場合のみ、 is による比較が行われます。 

  3. 本来 Python では文字列(str)やバイト列(bytes)はシーケンスとして扱われるのですが、 collections.abc.Sequence 類ではないため Sequence Patterns にはマッチしません。また iterable なオブジェクトについてもマッチしません。 

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
Sign upLogin
29
Help us understand the problem. What are the problem?