https://atmarkit.itmedia.co.jp/ait/articles/2110/08/news035.html [この記事はPDF出力に対応していません]
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とのマッチングを行うようにすることで、先ほどの失敗を回避している。
グループパターンは、パターンをかっこ「()」で囲んだもののことだ。後述するORパターンを使って複数のパターンをまとめるときに、関連のあるパターンをかっこで囲むといった使い方が考えられる。
num = 121
match num:
case (0 | 1) | (120 | 121): # 「case 0 | 1 | 120 | 12:」と同じ
print('foo')
case _:
print('bar')
シーケンスパターンはsubject valueがシーケンスのときにマッチするかどうかを調べる。このとき、シーケンスパターンの要素には上で見たパターン、または後述するパターンを記述できる。
以下は簡単な例だ。
match 10, 20, 30: # (10, 20, 30)というタプルを生成
case [x]: # 1要素のシーケンスにマッチして、変数xにその要素を代入
print(f'1 item: {x}')
case [x, y]: # 2要素のシーケンスにマッチして、変数xとyにその要素を代入
print(f'2 items: {x}, {y}')
case [0, 10, 20]: # 0、10、20を要素とするシーケンスにマッチ
print('3 items')
case [10, x, y]: # 3要素のシーケンスにマッチし、最後の2つの要素を変数に代入
print(f'3 items: 10, {x}, {y}')
この例ではシーケンスパターンは固定長だ。このときには、subject value(ここでは(10, 20, 30)というタプル)の長さとシーケンスパターンの長さが一致していないとマッチしない(そして、次のcaseブロックに移動する)。そのため、1要素と2要素のシーケンスパターンを記述している最初の2つのcaseブロックはマッチに失敗する。その後の2つのcaseブロックでは、3要素のシーケンスパターンが指定されている。そのため、このいずれかにマッチしてほしいところだ。
ただし、どちらにマッチするかを見る前に、サブパターンについて話をしておく。サブパターンとはあるパターンを構成するパターンのことだ。シーケンスパターンであれば、シーケンスを構成する個々の要素のことだと考えてよい。あるシーケンスパターンを構成する全てのサブパターンのマッチングが成功すれば、そのシーケンスパターンのマッチングは成功する。一方、サブパターンが1つでもマッチングに失敗すれば、そのシーケンスパターンのマッチングは失敗となる(後述するORパターンでは、サブパターンの1つでも成功すれば、そのパターンのマッチングは成功する)。
そして、1つ目のシーケンスパターンのサブパターンとは3つのリテラルパターン(0、10、20)のことだ。このサブパターンが左から、subject valueである[10, 20, 30]の先頭要素とそれぞれマッチングされる。0と10、10と20、20と30なので、これらはいずれもマッチングに失敗する。そのため、このcaseブロックは実行されない。
2つ目のシーケンスパターンは1つのリテラルパターン(10)と、2つのキャプチャーパターン(xとy)であり、先頭要素は10と10でマッチする。その後の2つはキャプチャーパターンなので常に成功して、変数xとyには対応する値が代入される。そのため、このmatch〜case文は「3 items: 10, 20, 30」と出力するはずだ。
シーケンスパターンには可変長のものもある。可変長のシーケンスパターンとは、そのサブパターンとして、アスタリスク「*」が前置されたキャプチャーパターンかワイルドカードパターンを含むもののことだ。これは関数の可変長引数と似たもので、subject valueであるシーケンスから任意の個数の要素をそのパターンに代入するものだ(ただし、「*_」では代入は行われない)。関数の可変長引数と異なるのは、アスタリスク付きのパターンはシーケンスパターンの任意の位置に置ける点だ。
以下に可変長のシーケンスパターンの使用例を示す(ここでは話を単純にするためにサブパターンは全てキャプチャーパターンとしてある)。
person = ['isshiki', 'tokyo', 'setagaya', 'kamikitazawa', '03-1111-1111']
match person:
case (name, *addr, tel):
print(addr) # ['tokyo', 'setagaya', 'kamikitazawa']
print(tel) # 03-1111-1111
この例では、シーケンスパターンの2つ目の要素(*addr)にアスタリスクがあり、telにはアスタリスクがない(実際、アスタリスク付きのサブパターンを複数記述することは許されていない)。これは元のシーケンスとシーケンスパターンの要素数を考慮して適切にキャプチャーを行ってくれるということだ。
なお、可変長のシーケンスパターンは、アスタリスク付きのサブパターンは空でもよいが、それよりも前にあるサブパターンには対応する要素がsubject valueに存在していなければマッチングに失敗することは覚えておこう(上のコードでpersonの要素が1つしかなければマッチングは成功するが、変数telは定義されない。また、personが空のシーケンスの場合はマッチングに失敗する)。
注意点としては、文字列やバイト列はれっきとしたPythonのシーケンスだが、シーケンスパターンではこれらはシーケンスとしては扱われないことが挙げられる。これらはリテラルパターンの値として使用される。
マッピングパターンは辞書のようなキー/値の組を持ったデータに対してマッチングを行う際に使用する。簡単な例を以下に示す。
class Dog:
def __init__(self, name, role):
self.name = name
self.role = role
class Cat:
def __init__(self, name, skill):
self.name = name
self.skill = skill
def getPet(arg):
match arg:
case {'type': 'dog', 'name': name, 'role': role}:
pet = Dog(name, role)
case {'type': 'cat', 'name': name, 'skill': skill}:
pet = Cat(name, skill)
case _:
pet = None
return pet
dog = {'type': 'dog', 'name': 'pochi', 'role': 'sentinel'}
cat = {'type': 'cat', 'name': 'mike', 'skill': 'lovely'}
pochi = getPet(dog)
mike = getPet(cat)
print(pochi.role) # sentinel
print(mike.skill) # lovely
この例では、辞書として格納されているペットのデータをgetPet関数内のmatch文で犬か猫かで処理を分岐させ、DogクラスかCatクラスのインスタンスが得られるようにしている。
マッピングパターンでは、キーと値の双方がサブパターンとなる(ただし、キーについてはリテラルパターンか値パターンのいずれかしか記述できない)。また、シーケンスパターンのアスタリスク付きのサブパターンと同様、ダブルアスタリスク「**」付きのサブパターンも記述できる。上のgetPet関数をこのサブパターンを使って記述したものを以下に示す。
def getPet(arg):
match arg:
case {'type': 'dog', **args}:
pet = Dog(**args)
case {'type': 'cat', **args}:
pet = Cat(**args)
case _:
pet = None
return pet
pochi = getPet(dog)
mike = getPet(cat)
print(pochi.role)
print(mike.skill)
シーケンスパターンではアスタリスク付きのサブパターンはシーケンス中の任意の位置に置けたが、マッピングパターンのダブルアスタリスク付きのサブパターンは最後の要素とする必要がある。
クラスパターンはsubject valueが何かのクラスのインスタンスかどうかや、インスタンスの属性が特定の値かどうかを調べるのに使用する。以下に例を示す。
match pochi:
case Cat():
print(f'{pochi.name} is a cat')
case Dog():
print(f"{pochi.name}'s role is a {pochi.role}")
この例では、上の例で定義した変数pochiがCatクラスのインスタンスかDogクラスのインスタンスかを調べている。
このインスタンスの属性の値を調べるのであれば、次のように記述する。
match pochi:
case Dog(name='mike', skill='lovely'):
print('pochi is in fact a cat')
case Dog(name='pochi', role='sentinel'):
print('pochi is pochi')
属性の値は「キーワード=パターン」のように記述する(これをキーワードパターンと呼ぶ)。そして、「キーワード=パターン」として指定された全ての属性について、パターンと実際の属性の値がマッチすればそのパターンのマッチングは成功する。
ただし、全ての属性をこのような形式で記述するのはなかなか面倒だ。そこでクラス定義にクラス属性として「__match_args__」を定義できる。
class Dog:
__match_args__ = ('name', 'role')
def __init__(self, name, role):
self.name = name
self.role = role
__match_args__クラス属性には以下に示す「位置パターン」で(「クラス名(属性値0, 属性値1, ……)」のように位置引数と同様な形式で)マッチするかどうかを調べたい属性を並べたときに、それがどの属性に対応するのかが分かるように、属性名(文字列)を要素とするタプルを記述する。タプルの先頭要素が、位置パターンの先頭要素の属性名として、タプルの次の要素が位置パターンの次要素の属性名として……のように使われる。
この機構を利用すると上記のコードは以下のように記述できる。
pochi = Dog('pochi', 'sentinel')
match pochi:
case Dog('mike', 'lovely'):
print('pochi is in fact a cat')
case Dog('pochi', 'sentinel'):
print('pochi is pochi')
ORパターンは、名前から分かるように、複数のパターンのマッチ結果の論理和が全体としてのマッチ結果となるパターンだ。つまり、複数のパターンのいずれかがマッチすれば、全体としてはマッチに成功したと考えられる。
num = 121
match num:
case 0 | 1 | 120 | 121:
print('foo')
case _:
print('bar')
これはnumの値が0、1、120、121のいずれかであればマッチするということだ(4つのサブパターンで構成され、それらは全てリテラルパターンとなっている)。
注意したいのは、ORパターンの中に(例えば、定数との比較のつもりで)キャプチャーパターンを記述してしまうと、それが常に成功してしまうので、そのORパターンは常に成功してしまうようになる点だ。
ASパターンは、これまでに見てきたパターンに「as 変数」と記述するパターンのこと。ASパターンを付加したパターンが成功すると、指定された変数にその値が代入される。例えば、サブパターンにORパターンを記述したときに、どの値がマッチしたかを記録する目的で使用できる。
order = {'drink': 'BEER', 'food': 'TON-KATSU'}
match order:
case {'drink': ('BEER' | 'SAKE' | 'COKE') as drink,
'food': ('TON-KATSU' | 'SASHIMI' | 'TO-FU') as food}:
print(f'order: {drink} and {food}')
この例ではマッピングパターンのサブパターンとして、注文された飲み物とフードを変数drinkとfoodにASパターンを使用して取り出すようにしている。
今回はPython 3.10で追加された構造的パターンマッチ(match〜case文)について駆け足で説明をした。次回はその他の機能の幾つかを紹介する予定だ。