Python Monthly Topics

Pythonの構造的パターンマッチングのさらに便利なパターン紹介

鈴木たかのり@takanoryです。今月の「Python Monthly Topics」では、第1回で紹介したPython 3.10の新機能「構造的パターンマッチング(Structural Pattern Matching⁠⁠」の続きをお届けします。

前回は構造的パターンマッチング全体の説明、いくつかのパターンをコード例を交えて紹介しました。今回はその続きとして、前回紹介できなかった他のパターンについても紹介します。

構造的パターンマッチングとは

前回の繰り返しになりますが、この記事で初めて構造的パターンマッチングを知った人に向けて、簡単に紹介します。詳細は上記の記事を参照してください。

構造的パターンマッチングはPython 3.10で新しく導入された文法です。Python 3.10は2021年10月にリリースされました。

新しいソフトキーワードとしてmatchcase_が増えました。これらのソフトキーワードを使用して、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_food()関数で注文できます。

シーケンスパターンとリテラルパターンを組み合わせる
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パターン|を組み合わせて以下のように指定します。

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変数にビールのサイズが代入されます。

ASパターンでサイズを変数に代入
order = ("beer", "IPA", "Pint")
# order = ("beer", "IPA", "Mass")  # 無効なサイズ

match order:
    case ["beer", style, "Pint" | "HalfPint" as size]:  # サイズをORパターンで定義
        order_beer(style, size)  # ビールを注文

組み込みクラスにマッチ

フードの注文をするときにはフードの名前(文字列)を指定し、水の注文をするときにはグラスの数(整数)のみを指定できるようにしたいです。その場合クラスパターンとASパターンを使用すると以下のように書けます。

ASパターンで組み込みクラスを指定
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などを見かけたら、パターンマッチングに書き換えるチャンスです!!

参考資料

おすすめ記事

記事・ニュース一覧