鈴木たかのり
前回は構造的パターンマッチング全体の説明、いくつかのパターンをコード例を交えて紹介しました。今回はその続きとして、前回紹介できなかった他のパターンについても紹介します。
構造的パターンマッチングとは
前回の繰り返しになりますが、この記事で初めて構造的パターンマッチングを知った人に向けて、簡単に紹介します。詳細は上記の記事を参照してください。
構造的パターンマッチングはPython 3.
新しいソフトキーワードとしてmatch
、case
と_
が増えました。これらのソフトキーワードを使用して、match
の後ろの式の結果に合致するcase
にマッチするという文法です。case
の後ろにさまざまなパターンが書けることが特徴です。
match beer_style: # Pilsner, IPA, Hazy IPA and others
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")
# フード「pizza」を作ります
order = ("water", 3)
# 水を3杯運びます
order = ("beer", "IPA", "Pint")
# 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_
関数で注文できます。
order = ("food", "nuts", "fries", "pizza")
match order:
case ["food", *foods]: # 複数のフードに対応
for food in foods: # foods変数には("nuts", "fries", "pizza")が代入される
order_food(food) # フードの注文
ORパターンとASパターン
この店ではビールのサイズは"Pint"
と"HalfPint"
しか指定できないとします。その場合はリテラルパターンとORパターン|
)
order = ("beer", "IPA", "Pint")
# order = ("beer", "IPA", "Mass") # 無効なサイズ
match order:
case ["beer", style, "Pint" | "HalfPint"]: # ビールのサイズをORパターンで定義
pass # ビールを注文
case ["beer", style, size]: # それ以外のサイズ
print(f"{size}は無効なサイズです。PintかHalfPintのみです")
しかし上記のコードでは、ビールのサイズがどちらかわかりません。そのような場合は、ASパターンを使用してサブパターン"Pint" | "HalfPint"
)size
変数にビールのサイズが代入されます。
order = ("beer", "IPA", "Pint")
# order = ("beer", "IPA", "Mass") # 無効なサイズ
match order:
case ["beer", style, "Pint" | "HalfPint" as size]: # サイズをORパターンで定義
order_beer(style, size) # ビールを注文
組み込みクラスにマッチ
フードの注文をするときにはフードの名前
order = ("food", "pizza")
# order = ("food", True) # フードの注文としてマッチしない
# order = ("water", 5)
# order = ("water", "ebian") # 水の注文としてマッチしない
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"}
# order = {"water": 3}
# order = {"beer": "IPA", "size": "Pint"}
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"}
# order = {"food": "fries", "size": "large"}
match order:
case {"food": str(food)}:
order_food(food) # フードの注文
また、辞書の残りの要素をキャプチャーして変数に代入もできます。その場合は変数名の前に**
を付けます。以下のコード例ではrest
変数には辞書{"type": "margherita"}
が代入されます。なお、余分な要素がない場合はrest
には空の辞書{}
)
order = {"food": "pizza", "type": "margherita"}
# order = {"food": "pizza"} # rest は {} となる
match order:
case {"food": str(food), **rest}:
order_food(food, rest) # rest = {"type": "margherita"}
ガード
最後にガードについて説明します。ガードはパターンの後ろにif
文を書くことで、そのif
文の結果がTrue
となるときだけパターンにマッチします。
たとえば水の注文で、0以下の数が指定できるのは適切ではありません。また、あまりたくさん水を注文されても困るので、上限を8とします。その場合、ガードを使うと以下のように書けます。
order = ("water", 3)
# order = ("water", 10) # 範囲外
# order = ("water", -1) # 範囲外
# order = ("water", "ebian") # 整数じゃない
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") # 正しい組み合わせ
# order = ("beer", "Pale Ale", "HalfPint") # 正しい組み合わせ
# order = ("beer", "Hazy IPA", "Pint") # スタイルが対象外
# order = ("beer", "Pilsner", "Mass") # サイズが対象外
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
などを見かけたら、パターンマッチングに書き換えるチャンスです!!