構造的パターンマッチのシーケンスパターンではさまざまなサブパターンを駆使することで柔軟な条件分岐が可能だ。その例と注意点を紹介する。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
# シーケンスパターン内でリテラルパターンとキャプチャーパターンを使用
cmd = 'move to (123, 456)'
cmd = cmd.replace('(', '').replace(')', '').replace(',', '').split()
print(cmd) # ['move', 'to', '123', '456']
match cmd:
case ['move', 'to', x, y]:
print(f'moved to ({x}, {y}).')
case _:
print('not matched.')
# シーケンスパターン内でリテラルパターンとスターパターンを使用
cmd = 'move to (123, 456)'.split()
match cmd:
case ('move', 'to', *rest):
for idx, item in enumerate(rest):
tmp = item.replace('(', '').replace(')', '').replace(',', '')
rest[idx] = tmp
print(f'moved to ({rest[0]}, {rest[1]}).')
case _:
print('not matched.')
# シーケンスパターン内ではスターパターンの後にもキャプチャーパターンを書ける
cmd0 = 'get gun bullet shield sword'.split()
cmd1 = 'get gun bullet shield and sword'.split()
cmds = [cmd0, cmd1]
for cmd in cmds:
match cmd:
case ['get', *items]:
print(f'got {" ".join(items)}')
case ['get', *items, 'and', rest]:
print(f'got {" ".join(items)}, plus {rest}')
# ただし、特殊なものを先に書く必要がある
for cmd in cmds:
match cmd:
case ['get', *items, 'and', rest]:
print(f'got {" ".join(items)}, plus {rest}')
case ['get', *items]:
print(f'got {" ".join(items)}')
# シーケンスパターン内でワイルドカードパターンも書ける
match cmd0: # 比較対象は['get', 'gun', 'bullet', 'shield', 'sword']
case ['get', *items, _]: # 'sword'が_にマッチする
print(f'got {" ".join(items)}')
match cmd0: # 比較対象は['get', 'gun', 'bullet', 'shield', 'sword']
case ['get', *_, 'sword']: # ワイルドカードパターンに*を付加することも可能
print('got sword and something')
Python 3.10で導入されたmatch文を使うと条件分岐をより強力、より柔軟に行える。その中でもシーケンスパターンはリストなどがどのような値を含んでいるかに応じて処理を分岐させるのに便利に使える。
シーケンスパターンに限らず、あるパターンの部分となるパターンのことを「サブパターン」と呼ぶ。以下はサブパターンとして、リテラルパターン(リテラルとの比較)とキャプチャーパターン(比較対象の値を変数に代入する)とを使用する例だ。
cmd = 'move to (123, 456)'
cmd = cmd.replace('(', '').replace(')', '').replace(',', '').split()
print(cmd) # ['move', 'to', '123', '456']
match cmd:
case ['move', 'to', x, y]:
print(f'moved to ({x}, {y}).')
case _:
print('not matched.')
この例では'move to (123, 456)'という文字列はかっこ「()」とカンマ「,」の削除が行われた後、空白文字を区切りとして分割され、['move', 'to', '123', '456']というリスト(シーケンス)に変換されている。
先頭の「case ['move', 'to', x, y]:」では、上記のリストが「['move', 'to', x, y]」というシーケンスパターンにマッチするかどうかを調べている。リストの内容と見比べてみると、最初の'move'と次の'to'はリテラルパターン(シーケンスパターンのサブパターン)でのマッチングであり、これはマッチする。残りの「x, y」という部分はキャプチャーパターンであり、リストに含まれている2つの要素「'123', '456'」がそれぞれ変数xとyにキャプチャーされる。
よって、このマッチングは成功し「moved to (123, 456).」と出力される。リストcmdの内容が「['move', 'to', '123']」だと変数yにキャプチャーされる要素がないので、マッチせずに「not matched.」と表示される。
シーケンスパターンでは、可変個の要素を扱うために「*」付きの変数も使用できる(これをPEP 634では「star_pattern」と表記しているので、ここでも「スターパターン」と呼ぶ)。これは関数やメソッドの可変長引数と同様なものだ。以下に例を示す。
cmd = 'move to (123, 456)'.split()
match cmd:
case ('move', 'to', *rest):
for idx, item in enumerate(rest):
tmp = item.replace('(', '').replace(')', '').replace(',', '')
rest[idx] = tmp
print(f'moved to ({rest[0]}, {rest[1]}).')
case _:
print('not matched.')
この例では、2つの変数xとyを使ったキャプチャーパターンではなく、*付きの変数restに2つの値をキャプチャーしようとしている。リストcmdの要素である'(123,'と'456)'はrestにキャプチャーされ['(123,', '456)']というリストが形成される。その後、このリストを反復して、文字列からかっことカンマが削除されたものがrestの対応する要素にセットされるようにしている(そのため、enumerate関数でリストのインデックスと要素を取り出している)。
マッチ対象が不特定多数の要素を含んでいる場合には、スターパターンを使うと、特定の意図で集められた不特定多数の要素を1つの変数にまとめて扱えるようになるだろう。
1つのシーケンス中でスターパターンは1つだけ使用できる。1つだけしか使えないが、どこに記述するかは自由だ。最後のサブパターンである必要はない。よって、次のようなパターンの書き方も許されている。
cmd0 = 'get gun bullet shield sword'.split()
cmd1 = 'get gun bullet shield and sword'.split()
cmds = [cmd0, cmd1]
for cmd in cmds:
match cmd:
case ['get', *items]:
print(f'got {" ".join(items)}')
case ['get', *items, 'and', rest]:
print(f'got {" ".join(items)}, plus {rest}')
この例では、最初のシーケンスパターン「['get', *items]」では'get'という文字列以外は全て変数itemsにキャプチャーされる。一方、次のシーケンスパターン「['get', *items, 'and', rest]」では可変個の要素がitemsにキャプチャーされた後、'and'がマッチして最後に残ったものがrestにキャプチャーされるようなパターンとなっている。
このように、スターパターンはシーケンスパターンの任意の位置に記述可能だ(書けるのは1つだけ)。だが、上のコードには落とし穴があって、これを実行すると次のような結果になる。
上のコードでは'get gun bullet shield and sword'を分割したときなど、リストに'and'が含まれているときには、2つ目のcase節で処理をして「got gun bullet shield, plus sword」のような出力を得られるようにしたかったのだが、実は最初のcase節で'get'以外の全ての要素がitemsにキャプチャーされてしまう。そのため、水色の背景色となっているテキスト「got gun bullet shield and sword」のように望んだ結果とは異なる出力になってしまっている。
match文での処理結果が想定と異なるときには、case節の記述順序に問題がないかを確認してみるのもよいだろう。
シーケンスパターンに限らないが、match文のcase節は特殊な条件からより広範な条件という順序で記述していく必要がある(これは例外処理でexcept節にはなるべく特殊な例外クラスを上の方に書くのと似た感じだ)。
この場合には、以下のようにcase節の順序を入れ替える必要があるということだ。
for cmd in cmds:
match cmd:
case ['get', *items, 'and', rest]:
print(f'got {" ".join(items)}, plus {rest}')
case ['get', *items]:
print(f'got {" ".join(items)}')
こうすれば、'and'を含むリストであれば、最初のcase節で処理が行われ、そうでなければ次のcase節で処理が行われるようになる。
なお、シーケンスパターンの中にワイルドカードパターンを書いてもよい。このときには、ワイルドカードパターンにマッチした要素については変数へのキャプチャーが行われなくなる(case節のブロックでマッチした要素を使う必要がなければ、ワイルドカードパターンを使うことが考えられる)。
以下に例を示す。
match cmd0: # 比較対象は['get', 'gun', 'bullet', 'shield', 'sword']
case ['get', *items, _]: # 'sword'が_にマッチする
print(f'got {" ".join(items)}') # got gun bullet shield
このとき、比較対象となっているのは['get', 'gun', 'bullet', 'shield', 'sword']というリストであり、_はその最後の要素である'sword'にマッチする(が、キャプチャーは行われない)。そのため、出力は「got gun bullet shield」となる。
可変長個の要素を捨てるのであれば、_の前に*を前置してもよい。以下に例を示す。
match cmd0: # 比較対象は['get', 'gun', 'bullet', 'shield', 'sword']
case ['get', *_, 'sword']: # ワイルドカードパターンに*を付加することも可能
print('got sword and something')
この例では、シーケンスパターンは['get', *_, 'sword']となっていて、'get'と'sword'の間にある要素は全て*_にマッチしてキャプチャーされることなく捨てられる。
Copyright© Digital Advantage Corp. All Rights Reserved.