Python 3.10で追加された構造的パターンマッチ(match〜case文)の概要と各種パターンの記述方法をサンプルコードと共に紹介する。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
2021年10月4日にPython 3.10がリリースされた。主要な新機能や変更点をかいつまんでまとめると以下のようになる(詳しくは「What's New In Python 3.10」を参照されたい)。
本稿では、これらの新機能の中で新しく追加された構造的パターンマッチ(match〜case文)について見ていくことにする。なお、構造的パターンマッチについてはPEP 634、PEP 635、PEP 636で詳しく述べられている。
match〜case文の構文を以下に示す。なお、「match」と「case」はソフトキーワードであり、構造的パターンマッチを行う文脈でのみキーワードとして識別される。他の場面ではmatchという識別子もcaseという識別子も定義はされていない。
match subject_expr:
case pattern_1:
pass # subject_exprの値がpattern_1にマッチした場合に行う処理
case pattern_2:
pass # subject_exprの値がpattern_2にマッチした場合に行う処理
# 省略
case pattern_n:
pass # subject_exprの値がpattern_nにマッチした場合に行う処理
case _:
pass # 全てのパターンにマッチしなかった場合に行う処理
match文ではsubject_exprに指定された式を評価して値を得る。この値を以下では「subject value」(対象の値)と呼ぶことにする。そして、subject valueが各caseブロックに記述されているパターンとマッチするかどうかが(上から順番に)調べられる。マッチすれば、そのcaseブロックに記述された処理が実行されて、match文が終了する。マッチしなかったときには次のcaseブロックに進み、subject valueがそのブロックのパターンにマッチするかどうかが調べられて……という具合に処理が進む。
最後の「case _:」というcaseブロックは、どんな値にもマッチする(他の構文要素におけるelse節に相当すると考えてもよいだろう)。そのため、それまでのパターンにマッチしなかったときにはこのブロックの処理が実行される。このブロックは省略でき、その場合には上のパターンにマッチしなかったときには何の処理も実行されない。また、マッチは上のcaseブロックから下のcaseブロックへと進むので、このような全ての値にマッチするcase節を最後以外の部分に書いてはいけない(書くと例外が発生する)。
簡単な例を以下に示す。
from random import randint
num = randint(1, 3)
match num:
case 2:
print(f'{num=}')
case _:
print(f'{num} != 2')
変数numには1、2、3のいずれかの値が代入される。match文では「subject_expr」が「num」なので、それを評価した値であるsubject valueは変数numの値となる。最初のcaseブロックではその値がリテラル値「2」にマッチするかどうかを調べている。マッチすれば、「num=2」のような出力が得られる。マッチしなければ、次のcaseブロックに移るが、これは上で見たように全ての式とマッチするので、変数numの値が1か3であれば、「1 != 2」「3 != 2」のような出力が得られる。
最初のパターンのようにリテラル値(数値、文字列、True、Falseなど)をそのまま記述したパターンのことを、見たままだが「リテラルパターン」と呼ぶ。また、最後のパターンのことを、何にでもマッチすることから「ワイルドカードパターン」と呼ぶ。
Pythonのif文を使えば、上記のコードは次のように書けるだろう。
from random import randint
num = randint(1, 3)
if num == 2:
print('f{num=}')
else:
print(f'{num} != 2')
このような単純な例だと、match〜case文のメリットが見えてこないかもしれない。が、match〜case文ではさまざまなパターンを記述できるようになっており、それらをうまく使うと、if文では複雑になるかもしれないコードをシンプルに記述できるようになる(後述)。
なお、subject_exprにはカンマ「,」区切りで複数の式を記述してもよい。
match 10, 20, 30: # match (10, 20, 30)
case 10, 20, 30:
print('10, 20, 30')
カンマ区切りで式を並べたときには、それらを要素とするタプルが作られて、そのタプルが各caseブロックのパターンにマッチするかどうかが調べられる。
最後にパターンには「ガード」を付加できる。ガードは、パターンのマッチングに成功した後に実行され、ガードに指定した式が真なら最終的にマッチングが成功し、偽なら失敗する。以下に例を示す。
num = 41
match num:
case num if num == 42:
print(f'{num}: an answear to the ultimate question')
case _:
print(f'{num}: not an answear to the ultimate question')
最初のcaseブロックでは、この後で説明しているキャプチャーパターンを使っている。ただし、変数numの内容を変数numに代入しているのでこの部分に意味はない。キャプチャーパターンは通常、常に成功するが、ここでは「if num == 42」というガードが付いている。変数numの値が42でなければ、このガードそしてこのパターンが失敗し、その下のワイルドカードパターンに処理が進む。if文のようなロジックをmatch〜case文でどうしても記述したければこのような書き方をすることになるかもしれない(が、それならif文を使うべきだろう)。
これまでに「リテラルパターン」と「ワイルドカードパターン」の2つのパターンを見た。match文に記述できるパターンとしては、この2つを含めて以下のようなものがある。
これらを組み合わせてパターンを記述することもできる。
リテラルパターンとワイルドカードパターンについては既に見たので、以下では残りのパターンについて簡単に見ていくことにしよう。
「キャプチャーパターン」はsubject valueを変数に代入するパターンだ。以下に例を示す。
num = 2
match num:
case x: # キャプチャーパターン。numの値がxに代入される
print(f'num: {num}, x: {x}') # num: 2, x: 2
キャプチャーパターンを上のように使った場合、ワイルドカードパターンと同様に必ずマッチするので、これについても他のパターンよりも先に書いてはいけない。例えば、以下のコードを実行すると「SyntaxError: name capture 'x' makes remaining patterns unreachable」という例外が発生する。
num = 2
match num:
case x: # キャプチャーパターン。numの値がxに代入される
print(f'num: {num}, x: {x}') # num: 2, x: 2
case 1: # 上のパターンが必ずマッチするのでこのコードが実行されることはない
print("can't reach")
では、ワイルドカードパターンとの違いがどこにあるかというと、subject valueを変数に代入するかどうかにある。
num = 2
match num:
case x:
print(x) # 2
match num:
case _:
print(_) # NameError: name '_' is not defined
キャプチャーパターンは実際には、他のパターン(シーケンスパターンやマッピングパターンなど)と組み合わせて使うことで、subject valueに含まれる特定の値を変数に代入するといった使われ方をすることが多いだろう。
以下に例を示す。
person_list = [['isshiki', 'tokyo', 'setagaya-ku', '03-xxxx-yyyy'],
['kawasaki', 'kanagawa', 'yokohama', '044-xxx-yyyy']]
tokyo = []
kanagawa = []
for item in person_list:
match item:
case [person, 'tokyo', *_]:
tokyo.append(person)
case [person, 'kanagawa', *_]:
kanagawa.append(person)
print(tokyo) # ['isshiki']
print(kanagawa) # ['kawasaki']
詳しくは説明しないが、この例では内側のリストの第1要素を名前として変数personにキャプチャーし、第2要素の地名でマッチングを行い(リテラルパターン)、住所が東京か神奈川かで別々のリストにキャプチャーした名前を追加している(その他の要素はワイルドカードパターンを使って読み捨てている)。if文を使えば、上のコードは次のように書けるだろう。
tokyo = []
kanagawa = []
for name, pref, *_ in person_list:
if pref == 'tokyo':
tokyo.append(name)
elif pref == 'kanagawa':
kanagawa.append(name)
print(tokyo)
print(kanagawa)
注意したいのは、Pythonには定数がないことだ。例えば、以下のコードはキャプチャーパターンになってしまう。
BEER = 0
SAKE = 1
COKE = 2
favor = input('0: Beer, 1: Sake, 2: Coke) ')
favor = int(favor)
match favor: # SyntaxError
case BEER:
print('you like beer')
case SAKE:
print('you may be really drunk')
case COKE:
print('i like coke, too')
BEER/SAKE/COKEは全て変数なので、これはキャプチャーパターンであり、最初の「case BEER:」が必ず成功してしまう(そのため、先ほどと同様に例外が発生する)。このようなときには、次に説明をする値パターンを使う必要がある。
値パターン(value pattern)というのは、subject valueをドット付きの名前、つまり何かの属性とマッチングするためのパターンである。例えば、上の例をPythonで列挙型をサポートするためのenumモジュールを使って書き直してみよう。
from enum import IntEnum, auto
class Drink(IntEnum):
BEER = 0
SAKE = auto()
COKE = auto()
favor = input('0: Beer, 1: Sake, 2: Coke) ')
favor = int(favor)
match favor:
case Drink.BEER:
print('you like beer')
case Drink.SAKE:
print('you may be really drunk')
case Drink.COKE:
print('i like coke, too')
値パターンでは「名前.属性名」のような形で、何らかのオブジェクトの属性をパターンとして記述して、subject valueにマッチするかどうかを調べる。上の例ではenum.IntEnumクラスの派生クラスを定義し、「Drink.BEER」のように属性アクセスを行うことでキャプチャーパターンではなく、属性の値とsubject valueとのマッチングを行うようにすることで、先ほどの失敗を回避している。
Copyright© Digital Advantage Corp. All Rights Reserved.