[解決!Python]match文で構造的パターンマッチを行うには:解決!Python
Python 3.10で追加されたmatch文は条件分岐をより柔軟形で行える。その基本構文と記述可能なパターンをざっくりと紹介する。
# 基本型
match subject_expr:
case pattern0 [if ...]:
# subject_exprがpattern0にマッチした(かつ、if句がTrueの)ときに行う処理
pass
case pattern1 [if ...]:
# subject_exprがpattern1にマッチした(かつ、if句がTrueの)ときに行う処理
pass
# 省略
case patternN [if ...]:
# subject_exprがpatternNにマッチした(かつ、if句がTrueの)ときに行う処理
pass
case _: # 全てにマッチする
# 上のパターンマッチが全て失敗したときにはワイルドカードパターンが成功する
# その場合に行う処理をワイルドカードパターンのブロックに記述する
pass
# リテラルパターン
values = [100, '100', True]
for value in values:
match value:
case 100:
print('int:', value)
case True:
print('bool:', value)
# ガード(if句)
somedata = 2
values = [100, '100', True]
for value in values:
match value:
case 100 if somedata == 0:
print('int:', value)
case '100':
print('str:', value)
case True if somedata == 2:
print('bool:', value)
# キャプチャーパターン
values = [100, 200, 300]
for value in values:
match value:
case 100:
print('100')
case 200:
print('200')
case captured: # キャプチャーパターンは最後に書く(必ず成功)
print(captured)
# ワイルドカードパターン
values = [100, '100', True]
for value in values:
match value:
case 100:
print('int:', value)
case True:
print('bool:', value)
case _: # ワイルドカードパターンは最後に書く(必ず成功)
print(f'matched to wildcard: {value}')
# ORパターン
values0 = [True, False, True, False]
values1 = [True, True, False, False]
for v0, v1 in zip(values0, values1):
match (v0, v1):
case (True, True) | (False, False):
print('2 items have same values')
print(v0, v1)
case (True, False) | (False, True):
print('2 items have not same values')
print(v0, v1)
# ASパターン
values = ['I do something', 'I do anything']
for value in values:
vs = value.split()
match vs:
case ['I' as s, 'do' as v, ('something' | 'anything') as o]:
print(f'S: {s}, V: {v}, O: {o}')
# バリューパターン(Value Pattern)
class Foo:
BAR = 100
BAZ = 200
values = [0, 100, 200, 300]
for value in values:
match value:
case Foo.BAR:
print(f'{value} matched to Foo.BAR({Foo.BAR})')
case Foo.BAZ:
print(f'{values} matched to Foo.BAZ({Foo.BAZ})')
case _:
print(f'{value} not matched')
# シーケンスパターン
mylists = ['I like Blue', 'I like yellow', 'I like white and red']
mylists = [s.split() for s in mylists]
print(mylists)
# mylistsの内容
# [['I', 'like', 'Blue'], ['I', 'like', 'yellow'], ['I', 'like', 'white', 'and', 'red']]
for l in mylists:
match l:
case ['I', 'like', ('red' | 'green' | 'yellow') as color]:
print('in 1st case')
print(f'you like {color}')
case ['I', 'like', color]:
print('in 2nd case')
print(f'you like {color}')
case ['I', 'like', *other]:
print('in 3rd case')
print(other)
colors = ' '.join(other)
print(f'you like {colors}')
# マッピングパターン
d0 = {'foo': 'FOO', 'bar': 'BAR'}
d1 = {'hoge': 'HOGE', 'hogehoge': 'HOGEHOGE'}
d2 = {'foo': 'FOO', 'bar': 'BAR', 'baz': 'BAZ'}
mydicts = [d0, d1, d2]
for d in mydicts:
match d:
case {'foo': 'FOO', 'bar': value, **items}:
print(value)
print(items)
case {**items}:
print(items)
# クラスパターン
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
points = [Point(0, 0), Point(1, 1), Point(2, 2)]
for p in points:
match p:
case Point(x=0, y=0):
print('origin')
case Point(1, 1):
print(f'({p.x}, {p.y})')
case _:
print('not matched')
match文と構造的パターンマッチ
Python 3.10からは構造的パターンマッチと呼ばれる条件分岐を行うためにmatch文が追加されている。なお、構造的パターンマッチの詳細についてはPEP 634、PEP 636を参照のこと。
match文の基本的な構文は次のようになる。
match subject_expr:
case pattern0 [if ...]:
# subject_exprがpattern0にマッチした(かつ、if句がTrueの)ときに行う処理
pass
case pattern1 [if ...]:
# subject_exprがpattern1にマッチした(かつ、if句がTrueの)ときに行う処理
pass
# 省略
case patternN [if ...]:
# subject_exprがpatternNにマッチした(かつ、if句がTrueの)ときに行う処理
pass
case _: # 全てにマッチする
# 上のパターンマッチが全て失敗したときにはワイルドカードパターンが成功する
# その場合に行う処理をワイルドカードパターンのブロックに記述する
pass
matchキーワードに続けて記述するのが条件分岐の基となる条件だ(subject_expr。以下では対象式とする)。これを評価した結果が、caseキーワードに続けて記述するパターン(上のpattern0など)にマッチすれば、そのブロックのコードが続けて実行される。マッチしなければ、次のcase節に移動して条件とパターンがマッチするかが調べられる。対象式が全てのパターンにマッチしなければmatch文では実質的には何のコードも実行されない(ただし、上のコードの最後のcase節のように、それまでに指定したパターン全てにマッチしなかった場合に実行するコードを記述することは可能だ)。
また、case節のパターンにマッチしても、さらに特定の条件が成立しているときにだけコードを実行したい場合がある。このようなときにはパターンに続けてif句を記述する。これをガードと呼ぶ。
match文では単純なリテラル比較、シーケンスの要素との比較、クラスの属性との比較など、さまざまなパターンを記述できる。また、マッチしたときに特定の値を変数に代入し、ブロック内でその値を使用するといったことも可能だ。
以下ではmatch文で記述できる各種パターンを簡単に紹介していく。
リテラルパターン
リテラルパターンは、文字通り、対象式(matchキーワードに続けて記述する式)とcaseキーワードに続けて記述したリテラルとを比較するものだ。以下に例を示す。
values = [100, '100', True]
for value in values:
match value:
case 100:
print('int:', value)
case True:
print('bool:', value)
# 出力結果:
#int: 100
#bool: True
リテラルパターンに記述できるのは数値、文字列、True、False、Noneのみである。上の例では3つの値を要素とするリストの各値(for文の中で変数valueに代入される)をmatch文のcase節で100、Trueと比較している。そのため、文字列の'100'についてはパターンにマッチしない。そのため、上の出力結果を見ると、整数値100とブーリアン値Trueについてのみ出力が行われている。
マッチはしたがさらに何らかの条件が成立したときにだけ処理を行いたいときにはif句によるガードが使える。以下に例を示す。
somedata = 2
values = [100, '100', True]
for value in values:
match value:
case 100 if somedata == 0:
print('int:', value)
case '100':
print('str:', value)
case True if somedata == 2:
print('bool:', value)
# 出力結果:
#str: 100
#bool: True
この例では文字列リテラルとの比較も含めた他、整数値とブーリアン値との比較についてはif句によるガードを記述してある。変数somedataの値は2となっているので、「case 100」でvalueとリテラルパターンがマッチするが、「if somedata == 0」が成立しないのでこのブロックのコードは実行されない。「case True if somedata == 2」はパターンもマッチし、ガードの条件も成立するのでコードが実行される。「case '100':」節についてはガードがないのでこれがマッチすれば、そのブロックのコードが実行される。このため、上の出力結果にあるように2つの出力が得られる。
キャプチャーパターン
キャプチャーパターンは、対象式の値をPythonの変数に代入する(Pythonの名前と対象式の値を束縛する)パターンだ。このパターンは常に成功するので、他のcase節よりも前に書いてはいけない(最後に書く必要がある)。
values = [100, 200, 300]
for value in values:
match value:
case 100:
print('100')
case 200:
print('200')
case captured: # キャプチャーパターンは最後に書く(必ず成功)
print(captured)
この例では、最初の2つのcase節はリテラルパターンで、整数値100、200との比較を行っている。リストvaluesの要素のうち2つは100と200なので、それらは最初の2つのcase節で処理されるが、300という要素はこれらにはマッチせずに、最後の「case captured:」節で300という値が変数capturedに代入される。代入された値は、そのブロックで使える(この場合は単にループ変数valueを使うだけでもよいが、パターンが複合的になったときにキャプチャーパターンにより対象式の一部の値だけを取り出すといった使い方をすることが多くなるだろう)。
ワイルドカードパターン
ワイルドカードパターンはキャプチャーパターンと同様に常に成功するパターンだ。これもmatch文では最後のパターンとして記述する必要がある。このパターンは、それまでに記述したパターンでマッチするものがなかったときに、何か処理をする必要があるときに記述する。キャプチャーパターンとの違いは、何らかの変数に値を代入することがないという点だ。
values = [100, '100', True]
for value in values:
match value:
case 100:
print('int:', value)
case True:
print('bool:', value)
case _: # ワイルドカードパターンは最後に書く(必ず成功)
print(f'matched to wildcard: {value}')
# 出力結果:
#int: 100
#matched to wildcard: 100
#bool: True
この例では、最後にワイルドカードパターンを記述している。これにより、上の2つのcase節にマッチしなかった場合(文字列'100')はこの節が実行されるようになる。そのため、上の出力結果のように3行の出力が行われる。
ORパターン
ORパターンは複数のパターンをバーティカルバー「|」でつなげるものだ。同じ処理を行えばよい複数の条件があるときに、それらを1つのパターンで記述するのが難しいときにはそれらを「|」でつなげていけばよい。
以下に例を示す。ここではTrue/Falseを含む2つのリストがあり、それらの対応する要素が同じ(TrueとTrue、FalseとFalse)かそうでないかで処理を分けたいものとする。
values0 = [True, False, True, False]
values1 = [True, True, False, False]
for v0, v1 in zip(values0, values1):
match (v0, v1):
case (True, True) | (False, False):
print('2 items have same values')
print(v0, v1)
case (True, False) | (False, True):
print('2 items have not same values')
print(v0, v1)
# 出力結果:
#2 items have same values
#True True
#2 items have not same values
#False True
#2 items have not same values
#True False
#2 items have same values
#False False
このときには2つのリストをzip関数でまとめて、それらの値をタプルとして受け取り、その組がTrue/TrueかFalse/Falseのどちらかにマッチするかどうかを調べている。このときに「(True, True) | (False, False)」のようにして2つのパターンをつなげている。
ASパターン
ASパターンは、マッチしたパターンを変数に代入するのに使える。以下に例を示す。
values = ['I do something', 'I do anything']
for value in values:
vs = value.split()
match vs:
case ['I' as s, 'do' as v, ('something' | 'anything') as o]:
print(f'S: {s}, V: {v}, O: {o}')
# 出力結果:
#S: I, V: do, O: something
#S: I, V: do, O: anything
この例では、2つの文字列「'I do something'」と「'I do anything'」があり、それらをmatch文で比較する前に空白で分割してリストにしている(実は、これは後で紹介するシーケンスパターンでもある)。case節では、「['I' as s, 'do' as v, ('something' | 'anything') as o]」のように'I'にマッチした部分を変数sに、'do'にマッチした部分を変数vに、'something'か'anything'にマッチした部分を変数oに代入している。このときには「as 変数名」のようにしている。これがASパターンと呼ばれるものだ。
なお、「('something' | 'anything')」という部分は先ほども見たバーティカルバーでリテラルパターンをつないだもの(ORパターン)をかっこ「()」で囲んでいる。このように、あるパターンが複数のパターンで構成されているとき、各部となるパターンのことをサブパターンと呼ぶ。また、かっこでパターンをくくって「これはひとまとまりのパターンである」と明示することをグループパターンと呼ぶ。
バリューパターン(Value Pattern)
バリューパターンは対象式を何かの属性と比較するときに使用する。典型的には(matchキーワードに続けて記述する)対象式をクラス定数やインスタンス変数とマッチするのに使われることになるだろう。以下に例を示す。
class Foo:
BAR = 100
BAZ = 200
values = [0, 100, 200, 300]
for value in values:
match value:
case Foo.BAR:
print(f'{value} matched to Foo.BAR({Foo.BAR})')
case Foo.BAZ:
print(f'{values} matched to Foo.BAZ({Foo.BAZ})')
case _:
print(f'{value} not matched')
# 出力結果:
#0 not matched
#100 matched to Foo.BAR(100)
#[0, 100, 200, 300] matched to Foo.BAZ(200)
#300 not matched
この例では、Fooクラスのクラス定数BAR(100)とBAZ(200)とリストの要素とを比較している。キャプチャーパターンのように属性に値が代入されるのではなく、属性と対象式(ここではリストの要素の値)との比較が行われている点に注意しよう。
シーケンスパターン
シーケンスパターンはリストやタプルなどの要素とマッチを行うのに使用する。このときパターンには角かっこ「[]」かかっこ「()」に囲んで、リテラルパターンやキャプチャーパターン、ORパターンなどを記述していく。「*変数名」のようにして、可変長の要素を1つの変数に代入することも可能だ。
以下に例を示す。ここでは3つの文字列「'I like Blue'」「'I like yellow'」「'I like white and red'」を空白文字で分割したリストを要素とするリストを用意している。
mylists = ['I like Blue', 'I like yellow', 'I like white and red']
mylists = [s.split() for s in mylists]
print(mylists)
# mylistsの内容
# [['I', 'like', 'Blue'], ['I', 'like', 'yellow'], ['I', 'like', 'white', 'and', 'red']]
for l in mylists:
match l:
case ['I', 'like', ('red' | 'green' | 'yellow') as color]:
print('in 1st case')
print(f'you like {color}')
case ['I', 'like', color]:
print('in 2nd case')
print(f'you like {color}')
case ['I', 'like', *other]:
print('in 3rd case')
print(other)
colors = ' '.join(other)
print(f'you like {colors}')
# 出力結果:
#in 2nd case
#you like Blue
#in 1st case
#you like yellow
#in 3rd case
#['white', 'and', 'red']
#you like white and red
最初のパターンは'I'と'like'というリテラルパターンと「('red' | 'green' | 'yellow') as color」というORパターンとASパターンを組み合わせたパターンが含まれている。このとき、リストの2つ目の要素「['I', 'like', 'yellow']」というリストは最初の2つの要素「'I'」「'like'」がリテラルパターンとマッチし、「('red' | 'green' | 'yellow') as color」というパターン('red'か'green'か'yellow'のどれか)にマッチして、その内容が変数colorに代入される。そのため、「you like yellow」という出力が得られる。
これに対して1つ目の「['I', 'like', 'Blue']」は最初のcase節の'I'と'like'にはマッチするが、最後のパターンにはマッチしないので、次のcase節に進む。このパターン(['I', 'like', color])では'I'と'like'にマッチして、最後のキャプチャーパターンにもマッチし変数colorに'Blue'が代入される。
最後の「['I', 'like', 'white', 'and', 'red']」では上の2つのcase節にはマッチしないが、「['I', 'like', *other]」というパターンにマッチして、変数otherには'I'と'like'を除いた要素['white', 'and', 'red']が変数otherに代入される。これを文字列のjoinメソッドで連結することで、最後には「you like white and red」という出力が得られる。
マッピングパターン
マッピングパターンは辞書などのキーと値の組を取り扱うためのパターンだ。シーケンスパターンにおけるアスタリスク付きの変数と同様に「**」付きの変数を用いることで可変長の要素を代入することも可能だ。
以下に例を示す。
d0 = {'foo': 'FOO', 'bar': 'BAR'}
d1 = {'hoge': 'HOGE', 'hogehoge': 'HOGEHOGE'}
d2 = {'foo': 'FOO', 'bar': 'BAR', 'baz': 'BAZ'}
mydicts = [d0, d1, d2]
for d in mydicts:
match d:
case {'foo': 'FOO', 'bar': value, **items}:
print(value)
print(items)
case {**items}:
print(items)
# 出力結果:
#BAR
#{}
#{'hoge': 'HOGE', 'hogehoge': 'HOGEHOGE'}
#BAR
#{'baz': 'BAZ'}
この例では、3つの辞書を要素とするリストを用意している。最初のcase節では'foo'というキーとその値'FOO'、'bar'というキーとその値(これはキャプチャーパターンなので何でもよい)、さらに可変長のマッピング値にマッチするかどうかをチェックしている。次のcase節では全てを変数itemsに取り込んでいる。
「{'foo': 'FOO', 'bar': 'BAR'}」と「{'foo': 'FOO', 'bar': 'BAR', 'baz': 'BAZ'}」という2つの辞書オブジェクトでは最初のcase節にマッチして、変数valueには'BAR'が代入される。1つ目の辞書オブジェクトでは他の要素はないので、変数itemsは空だが、2つ目の辞書オブジェクトではマッチした残りの「{'baz': 'BAZ'}」が代入される。
「{'hoge': 'HOGE', 'hogehoge': 'HOGEHOGE'}」という辞書オブジェクトは1つ目のcase節にはマッチしないが、2つ目のcase節では可変長の変数itemsに全てが代入される。
マッピングパターンではキーの部分にはリテラルパターンかバリューパターンしか記述できないことには注意しよう。
クラスパターン
クラスパターンはあるクラスのインスタンスとその属性とのマッチを行うために使用する。
以下に例を示す。ここではPointクラスの定義にdataclassデコレーターを使っている。dataclassについては「データクラスを定義するには」「データクラスのフィールドの初期化をカスタマイズするには」を参照のこと。
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
points = [Point(0, 0), Point(1, 1), Point(2, 2)]
for p in points:
match p:
case Point(x=0, y=0):
print('origin')
case Point(1, 1):
print(f'({p.x}, {p.y})')
case _:
print('not matched')
# 出力結果:
#origin
#(1, 1)
#not matched
ここではPointクラスを定義して、そのインスタンスが持つ属性xとyが特定の条件にマッチするかを試している。このときには、最初のcase節で行っているようにキーワード引数形式で属性を指定することも、2つ目のcase節で行っているように位置指定引数のように属性を指定することもできる(ただし、位置指定引数形式の属性指定ではクラスが持つ__match_args__属性の値を使って、位置指定形式からキーワード形式に属性が変換される)。
いずれにせよ、対象式がクラスパターンに指定したクラスのインスタンスであり、その属性の値がクラスパターンで指定した属性の値とマッチすれば、そのcase節のブロックが実行される。
この例ではPoint(0, 0)、Point(1, 1)、Point(2, 2)という3つのインスタンスを生成しているが、最初の2つは2つあるクラスパターンのいずれかにマッチして、最後のインスタンスはワイルドカードパターンにマッチする。
Copyright© Digital Advantage Corp. All Rights Reserved.