Python 3.10の新機能:「構造的パターンマッチ」とは:Python最新情報キャッチアップ(2/2 ページ)
Python 3.10で追加された構造的パターンマッチ(match〜case文)の概要と各種パターンの記述方法をサンプルコードと共に紹介する。
グループパターン
グループパターンは、パターンをかっこ「()」で囲んだもののことだ。後述する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パターン
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 変数」と記述するパターンのこと。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文)について駆け足で説明をした。次回はその他の機能の幾つかを紹介する予定だ。
Copyright© Digital Advantage Corp. All Rights Reserved.