鈴木たかのり(@takanory )です。今月の「Python Monthly Topics」では、第1回で紹介したPython 3.10の新機能「構造的パターンマッチング(Structural Pattern Matching) 」の続きをお届けします。
前回は構造的パターンマッチング全体の説明、いくつかのパターンをコード例を交えて紹介しました。今回はその続きとして、前回紹介できなかった他のパターンについても紹介します。
構造的パターンマッチングとは
前回の繰り返しになりますが、この記事で初めて構造的パターンマッチングを知った人に向けて、簡単に紹介します。詳細は上記の記事を参照してください。
構造的パターンマッチングはPython 3.10で新しく導入された文法です。Python 3.10は2021年10月にリリースされました。
新しいソフトキーワード としてmatch
、case
と_
が増えました。これらのソフトキーワードを使用して、match
の後ろの式の結果に合致するcase
にマッチするという文法です。case
の後ろにさまざまなパターン が書けることが特徴です。
構造的パターンマッチングの例
match beer_style:
case "Pilsner" :
result = "First drink"
case "IPA" :
result = "I like it"
case "Hazy IPA" :
result = "Cloudy and cloudy"
case _:
result = "I like most beers"
さまざまなパターン
上の例では文字列(リテラルパターン )とワイルドカードパターン (_
)のみを使用していますが、ほかにもさまざまなパターンが存在します。前回の記事ではキャプチャーパターン とクラスパターン を紹介しました。今回はそれ以外のパターンについて紹介します。
シーケンスパターン
シーケンスパターン はリストやタプルなどにマッチするパターンです。前回の例と同様にビールやフードを注文したいと思います。以下のように注文情報がタプルに格納されるとします。
注文のタプルの例
order = ("bill" ,)
order = ("food" , "pizza" )
order = ("water" , 3 )
order = ("beer" , "IPA" , "Pint" )
シーケンスパターンでは以下のように書くと、任意の長さのシーケンスにマッチします。
シーケンスパターンで注文を分岐
match order:
case [order_type]:
print ("会計します" )
case [order_type, param]:
if order_type == "food" :
print (f"フード「{param} 」を作ります" )
elif order_type == "water" :
print (f"水を{param} 杯運びます" )
case [order_type, style, size]:
print (f"{style} のビールを{size} サイズで注ぎます" )
case _:
print ("正しく注文してください" )
先ほどの「注文タプルの例」をorder
変数に代入した場合、結果は以下のようになります。
order = ("bill" ,)
order = ("food" , "pizza" )
order = ("water" , 3 )
order = ("beer" , "IPA" , "Pint" )
ただ、このままだと("food", "nuts", "pizza")
のような注文が、シーケンスの長さが3のためビールの注文とみなされてしまいます。以下のようにシーケンスパターンとリテラルパターンを組み合わせると、「 最初の要素が"food"
で長さ2のシーケンス」のように、より厳密に指定できます。
シーケンスパターンとリテラルパターンを組み合わせる
match order:
case ["bill" ]:
case ["food" , food]:
order_food(food)
case ["water" , number]:
order_water(number)
case ["beer" , style, size]:
order_beer(style, size)
case _:
print ("正しく注文してください" )
任意の長さのシーケンスにマッチ
("food", "nuts", "fries", "pizza")
のように、一度に複数のフードを注文したいとします。その場合はキャプチャ対象の変数に*
を付けることで、任意の長さのシーケンスにマッチできます。
foods
変数をfor
文で処理することによって、フードを1つずつに分割してorder_food()
関数で注文できます。
シーケンスパターンとリテラルパターンを組み合わせる
order = ("food" , "nuts" , "fries" , "pizza" )
match order:
case ["food" , *foods]:
for food in foods:
order_food(food)
ORパターンとASパターン
この店ではビールのサイズは"Pint"
と"HalfPint"
しか指定できないとします。その場合はリテラルパターンとORパターン (|
)を組み合わせて以下のように指定します。
ORパターンで任意のビールのサイズにのみ対応
order = ("beer" , "IPA" , "Pint" )
match order:
case ["beer" , style, "Pint" | "HalfPint" ]:
pass
case ["beer" , style, size]:
print (f"{size} は無効なサイズです。PintかHalfPintのみです" )
しかし上記のコードでは、ビールのサイズがどちらかわかりません。そのような場合は、ASパターン を使用してサブパターン(ここでは"Pint" | "HalfPint"
)にマッチした値を変数に代入します。以下のように書くとsize
変数にビールのサイズが代入されます。
ASパターンでサイズを変数に代入
order = ("beer" , "IPA" , "Pint" )
match order:
case ["beer" , style, "Pint" | "HalfPint" as size]:
order_beer(style, size)
組み込みクラスにマッチ
フードの注文をするときにはフードの名前(文字列)を指定し、水の注文をするときにはグラスの数(整数)のみを指定できるようにしたいです。その場合クラスパターンとASパターンを使用すると以下のように書けます。
ASパターンで組み込みクラスを指定
order = ("food" , "pizza" )
match order:
case ["food" , str () as food]:
order_food(food)
case ["water" , int () as number]:
order_water(number)
組み込みクラスの場合はstr() as food
の部分をstr(food)
のように書けます。よりコンパクトになるのでおすすめです。
組み込みクラスにマッチ
match order:
case ["food" , str (food)]:
order_food(food)
case ["water" , int (number)]:
order_water(number)
マッピングパターン
マッピングパターン は辞書のようなマッピング型にマッチします。JSONをパースした辞書オブジェクトをマッチするのに便利です。
たとえば、辞書形式で注文をする場合は以下のような形式になります。
マッピングパターン
order = {"food" : "pizza" }
match order:
case {"food" : str (food)}:
order_food(food)
case {"water" : int (number)}
order_water(number)
case {"beer" : style, "size" : ("Pint" | "HalfPint" ) as size}:
order_beer(style, size)
case _:
print ("正しく注文してください" )
マッピングパターンはシーケンスパターンと異なり、辞書データに余分な要素が存在してもマッチします。以下の例では余分なピザの種類が指定してありますが、フードの注文としてマッチします。
辞書の余分な要素は無視される
order = {"food" : "pizza" , "type" : "margherita" }
match order:
case {"food" : str (food)}:
order_food(food)
また、辞書の残りの要素をキャプチャーして変数に代入もできます。その場合は変数名の前に**
を付けます。以下のコード例ではrest
変数には辞書{"type": "margherita"}
が代入されます。なお、余分な要素がない場合はrest
には空の辞書({}
)が代入されます。
辞書の余分な要素をキャプチャーする
order = {"food" : "pizza" , "type" : "margherita" }
match order:
case {"food" : str (food), **rest}:
order_food(food, rest)
ガード
最後にガード について説明します。ガードはパターンの後ろにif
文を書くことで、そのif
文の結果がTrue
となるときだけパターンにマッチします。
たとえば水の注文で、0以下 の数が指定できるのは適切ではありません。また、あまりたくさん水を注文されても困るので、上限を8 とします。その場合、ガードを使うと以下のように書けます。
ガードで水の数を制限
order = ("water" , 3 )
match order:
case ["water" , int (number)] if 0 < number < 9 :
order_water(number)
case ["water" , int (_)]:
print ("水は1〜8杯の範囲で注文してください" )
case ["water" , _]:
print ("水の数は整数で指定してください" )
また、ビールのスタイルとサイズがいくつかの種類に制限されている場合、ORパターンでも実現可能ですが、ガードを使うとシンプルに書けます。
ガードでビールのスタイルを制限
STYLES = ("IPA" , "Pilsner" , "Pale Ale" , "Sour" )
SIZES = ("Pint" , "HalfPint" )
order = ("beer" , "IPA" , "Pint" )
match order:
case ["beer" , style, size] if style in STYLES and size in SIZES:
order_beer(style, size)
case ["beer" , style, size] if style not in STYLES:
print (f"スタイルは{STYLES} のみです" )
case ["beer" , style, size] if size not in SIZES:
print (f"サイズは{SIZES} のみです" )
まとめ
構造的パターンマッチングについて、前回の記事で紹介しなかったパターンやガードを中心に解説しました。いろいろなパターンがあり、強力な機能だということが伝わったでしょうか?
Pythonでプログラムを書いているときに、複雑でわかりにくいif
文を見たときには、ぜひパターンマッチングで書き直すことに挑戦してみてください。if
文の条件でlen()
、isinstance()
、hasattr()
、"key" in dct
などを見かけたら、パターンマッチングに書き換えるチャンスです!!
参考資料