構造的パターンマッチ

【Python】構造的パターンマッチ(match case)の使い方

【Python】構造的パターンマッチ(match case)の使い方

Python 3.10で追加された構造的パターンマッチ(match caseの使い方について解説します。

Pythonの構造的パターンマッチ

Python 3.10で新しく追加されたものとして「構造的パターンマッチ (Structural Pattern Matching)」という機能があります。

構造的パターンマッチは、その名の通り評価対象がパターンにマッチするかをチェックして、一致した場合の挙動を定義することができます。構造的パターンマッチは、近年の新機能の中で最も議論を起こした複雑な機能であると言われており、この機能は受理されるまで何度も議論されています。

関連する主なPEP文書であるPEP634, 635, 636の文書は以下になります。

構造的パターンマッチには多くのパターンがPEP634で定義されています。本記事では、PEPで定義されているパターンの例を使いつつ、構造的パターンマッチの基本的な使い方を紹介します。

構造的パターンマッチの基本的な使い方 (match case)

構造的パターンマッチでは、matchcaseというソフトキーワードが導入されており、match文として使用します。構文は以下のようになります。

構造的パターンマッチ
match expression:
    case pattern:
        ...

expressionの部分にはPythonの式を指定し、patternの部分にはマッチさせるパターンを指定します。複数パターンの場合は、複数caseを記載していきます。caseブロック内の…部分にはパターンに一致した場合のコードを記述します。

match文は、C言語のswitch文に似ていますが、後述するようにシーケンスやクラスのインスタンス内部の値との一致を確認できるなど強力な機能として使用できます。

以下は、他のプログラミングでも出てくるような簡単な分岐処理を、match文で実現した例です。

value = 2

match value:
    case 1:
        print("失敗")
    # ↓ ここがマッチする
    case 2:
        print("成功")
    case _:
        print("値は不適切です。")
【実行結果】
成功

上記ではvalue=2なので「case 2:」の部分がマッチして「成功」と表示されます。もしvalue=1の場合は「失敗」という表示になります。なお、match caseでは、一番最初にマッチしたcase以外の処理は実行されませんので注意しましょう。

また、”_“(アンダースコア)は、後述しますがワイルドカードパターン(Wildcard Patterns)で、必ず一致するパターンとして使用できます。上記例では、1や2以外の数値や文字列等の場合には「値は不適切です。」と表示されます。C言語のswitch文におけるdefaultのようなものです。

上記例は非常に簡単な例として紹介しましたが、Pythonの構造的パターンマッチのパターンの種類としては多くの種類があり、PEP634には「XXX Patterns」というように記載があります。以降では、代表的なパターンについて簡単な例を使って紹介してみたいと思います。

Note

予約語のことをキーワードと言いますが、ソフトキーワードはすべての文脈ではなく一部の文脈でのみのものになります。そのため、ソフトキーワードであるmatchcaseは、match文の文脈以外では、通常の変数や関数名として使用することができます。

構造的パターンマッチの種類

Pythonの構造的パターンマッチの各種パターンについて、例を使って紹介していきます。

リテラルパターン(Literal Patterns)

最も基本的な、caseの後ろにリテラルを記述するパターンをリテラルパターン(Literal Patterns)と言います。リテラルパターンでは、以下のように単純に値が一致するかどうかでマッチングを行います。

def check(data):
    match data:
        case 1:
            print("失敗")
        case 2:
            print("成功")
        case _:
            print("値は不適切です。")


if __name__ == "__main__":
    value = 1
    check(value)

    print("---")
    value = 2
    check(value)

    print("---")
    value = 3
    check(value)

    print("---")
    value = "test"
    check(value)
【実行結果】
失敗
---
成功
---
値は不適切です。
---
値は不適切です。

上記例では、1であれば失敗、2であれば成功、その他の場合は不適切な値として処理するようになっています。もちろん、ifelifelseを使った分岐でも実装できます。

キャプチャパターン(Capture Patterns)

キャプチャパターン(Capture Patterns)は、caseの後ろの変数に値をキャプチャして処理をするパターンです。

def check(data):
    match data:
        case 1:
            print("失敗")
        case x:
            print(f"成功: {x}")

    # 以下のような書き方をしてしまうと「case 1:」に到達しない
    # match value:
    #     case x:
    #         print(f"成功: {x}")
    #     case 1:
    #         print("失敗")


if __name__ == "__main__":
    value = 1
    check(value)

    print("---")
    value = 2
    check(value)

    print("---")
    value = 3
    check(value)

    print("---")
    value = "test"
    check(value)
【実行結果】
失敗
---
成功: 2
---
成功: 3
---
成功: test

上記例では「case 1:」の条件に一致しなかったdataを、変数xにキャプチャしています。そのため、print文ではxを使って値を表示できています。

キャプチャパターンで注意するべきことはcaseの順番です。match caseでは、一番最初にマッチしたcase以外の処理は実行されません。

    # 以下のような書き方をしてしまうと「case 1:」に到達しない
    match value:
        case x:
            print(f"成功: {x}")
        case 1:
            print("失敗")

例でコメントアウトしてある部分のように、caseの記載順番を変えて「case x:」を先に持ってきてしまうと「case 1:」に到達しなくなるので注意してください。

ワイルドカードパターン(Wildcard Patterns)

ワイルドカードパターン(Wildcard Patterns)は、caseの後ろに「_」を記述して、すべてのパターンをとらえるためのパターンです。既にこれまでの例でも、目的のパターンに一致しないものに対する処理を記述するためにワイルドカードパターンを使用しています。

def show(data):
    match data:
        case _:
            print(data)


if __name__ == "__main__":
    value = 1
    show(value)

    print("---")
    value = "test"
    show(value)
【実行結果】
1
---
test

上記例は、あまり意味のあるコードではありませんが、数値や文字列でもマッチしていることが分かるかと思います。

キャプチャパターンと似ていますが、違いは値をキャプチャしないことです。Pythonでは、for文など後々不要な値を表現する際に「_」を使いますが、それと同様です。

値パターン(Value Patterns)

値パターン(Value Patterns)は、既に定義されている変数や定数の名前にマッチするパターンです。

以下例は、Enumを使った列挙型の値との一致を確認する例になっています。なお、Enumについては「列挙型enumの使い方の基本」でまとめていますので興味があれば参考にしてください。

from enum import Enum, auto


class Color(Enum):
    RED = auto()
    GREEN = auto()
    BLUE = auto()
    YELLOW = auto()


def check(target):
    match target:
        case Color.RED:
            print("赤")
        case Color.GREEN:
            print("緑")
        case Color.BLUE:
            print("青")
        case _:
            print("一致はありません")


if __name__ == "__main__":
    color = Color.RED
    check(color)

    print("---")
    color = Color.GREEN
    check(color)

    print("---")
    color = Color.BLUE
    check(color)

    print("---")
    color = Color.YELLOW
    check(color)
【実行結果】
赤
---
緑
---
青
---
一致はありません

上記例では、列挙体であるColorクラスの値を使って色の判定をしています。

シーケンスパターン(Sequence Patterns)

シーケンスパターン(Sequence Patterns)は、listtupleといったシーケンス関連にマッチさせるために使用します。

以下の例は、対象の値が3で割り切れるか、5で割り切れるかをリストで表現しつつ、一致の確認を行っている例です。

def devisible_by_3_5(data):
    sequence = [data % 3, data % 5]
    match sequence:
        case (0, 0):
            print(f"{data}: 3と5で割り切れる")
        case (0, _):
            print(f"{data}: 3で割り切れる")
        case (_, 0):
            print(f"{data}: 5で割り切れる")
        case _:
            print(f"{data}")


if __name__ == "__main__":
    for d in range(1, 16):
        devisible_by_3_5(d)
【実行結果】
1
2
3: 3で割り切れる
4
5: 5で割り切れる
6: 3で割り切れる
7
8
9: 3で割り切れる
10: 5で割り切れる
11
12: 3で割り切れる
13
14
15: 3と5で割り切れる

上記例では、判定の前に「sequence = [data % 3, data % 5]」で、3で割った余りと5で割った余りのリストを作成しています。

case文では、sequenceが「(0, 0)」「(0, _)」「(_, 0)」「_」のどの形式にマッチするかを確認しています。「_」については、ワイルドカードパターンなのでどんな値や型でもよい部分です。

今回対象のsequenceはリストですが、caseでは「(0, 0)」といったタプルのような記載でも一致を確認できます。このようにリストやタプルといったシーケンスの構造に対して一致を確認することが可能です。

マッピングパターン(Mapping Patterns)

マッピングパターン(Mapping Patterns)は、辞書(dict)等のマッピング関連の一致を確認するために使用します。

以下は、条件を記載している辞書に対してキーで一致させて、値を表示しているような例になっています。

def check(target):
    match target:
        case {"cond1": cond1, "cond2": cond2}:
            print(cond1, cond2)
        case {"cond3": cond3, **items}:
            print(cond3)
            print(items)
        case {**items}:
            print(items)
        case _:
            print("対象外")


if __name__ == "__main__":
    data = {"cond1": 10, "cond2": 20}
    check(data)

    print("---")
    data = {"cond3": 30, "cond4": 40, "cond5": 50}
    check(data)

    print("---")
    data = {"cond6": 60, "cond7": 70}
    check(data)

    print("---")
    data = [1, 2, 3, 4, 5]
    check(data)
【実行結果】
10 20
---
30
{'cond4': 40, 'cond5': 50}
---
{'cond6': 60, 'cond7': 70}
---
対象外

上記例では、"cond1""cond2"といったキーで一致を確認しています。caseでキーが一致した場合には、cond1cond2といった変数に値がキャプチャされます。

case {"cond3": cond3, **items}:」という部分では、「**items」という表現を使っています。これは関数やメソッドでキーワード引数をまとめて受け取る**kwargsの考え方と同じでその他の項目をキャプチャします。"cond3"が一致する場合は、cond3に値をキャプチャしつつ、その他の項目はitemsにキャプチャされます。同様に「case {**items}:」としている部分では、それよりも上のcaseで一致しなかった辞書が全てitemsにキャプチャされます。

ワイルドカードパターンについては、辞書以外が指定されてきた場合の処理を記載するために使用できます。設定やデータなどが辞書で条件の一致を確認したいとき等に使用すると便利な場合があります。

クラスパターン(Class Patterns)

クラスパターン(Class Patterns)は、クラスのオブジェクトに対して、その属性で一致を確認するパターンです。

以下例では、Pointという2次元の位置を表すデータクラスを用意して、値で一致を確認する例を紹介します。なお、クラス定義では@dataclassデコレータを使用しています。@dataclassデコレータについては「dataclassの使い方の基本」でまとめていますので興味があれば参考にしてください。

キーワード引数でパターンマッチ (dataclass)

以下は、Pointクラスのインスタンスの一致をキーワード引数を使ってマッチングする例です。

from dataclasses import dataclass


@dataclass
class Point:
    """位置クラス"""

    x: int
    y: int


def where_is(point):
    match point:
        case Point(x=0, y=0):
            print(f"原点: {point}")
        case Point(x=0, y=y):
            print(f"Y={y}: {point}")
        case Point(x=x, y=0):
            print(f"X={x}: {point}")
        case Point():
            print(point)
        case _:
            print("Pointではない")


if __name__ == "__main__":
    pt1 = Point(0, 0)
    where_is(pt1)

    print("---")
    pt2 = Point(0, 5)
    where_is(pt2)

    print("---")
    pt3 = Point(5, 0)
    where_is(pt3)

    print("---")
    pt4 = Point(10, 10)
    where_is(pt4)

    print("---")
    pt5 = (20, 20)
    where_is(pt5)
【実行結果】
原点: Point(x=0, y=0)
---
Y=5: Point(x=0, y=5)
---
X=5: Point(x=5, y=0)
---
Point(x=10, y=10)
---
Pointではない

上記例では、Pointクラスのインスタンスの内部まで構造の一致を確認していて、色々なことが起こっています。caseの部分を表に整理してみます。

case一致する内容
case Point(x=0, y=0):Pointクラスのインスタンスであり、かつ、x=0, y=0である場合。
case Point(x=0, y=y):Pointクラスのインスタンスであり、かつ、x=0である場合。yの値は変数yにキャプチャされる。
case Point(x=x, y=0):Pointクラスのインスタンスであり、かつ、y=0である場合。xの値は変数xにキャプチャされる。
case Point():Pointクラスのインスタンスである場合。
case _:その他で常にマッチするワイルドカードパターン

クラスパターンでは、上記で紹介したようないくつかのパターンが混ざって動作をしています。インスタンス内の属性の値でマッチをとりつつ、必要に応じて変数に値をキャプチャしています。最後にはワイルドカードパターンも使っています。

Point(x=0, y=0)といった記載はコンストラクタ呼び出しをイメージする方が多いかと思いますが、caseで指定される場合はコンストラクタが呼び出されることはありません。この辺りは最初、混乱する部分かもしれません。

位置引数でパターンマッチ (dataclass)

上記でキーワード引数の形式で一致を見てきましたが、位置引数の形式で「case Point(0, 0):」「case Point(0, y):」「case Point(x, 0):」というように指定することも可能です。

def where_is(point):
    match point:
        case Point(0, 0):
            print(f"原点: {point}")
        case Point(0, y):
            print(f"Y={y}: {point}")
        case Point(x, 0):
            print(f"X={x}: {point}")
        case Point():
            print(point)
        case _:
            print("Pointではない")
【実行結果】
原点: Point(x=0, y=0)
---
Y=5: Point(x=0, y=5)
---
X=5: Point(x=5, y=0)
---
Point(x=10, y=10)
---
Pointではない

ただし、位置引数を使用する場合は注意が必要です。位置引数の形式を使うには、クラスに__match_args__の定義が必要になります。

上記の@dataclassデコレータを使った例では、データクラスでは、__match_args__をデフォルトで自動作成します。そのため、特に意識することなく位置引数の形式で使用できています。

コンストラクタなどを含めて自分で定義する場合には、以下のように__match_args__を明示的に定義する必要があるので注意しましょう。

class Point:
    """位置クラス"""

    # 属性の順序を__match_args__に追加する必要がある
    __match_args__ = ("x", "y")

    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point(x={self.x}, y={self.y})"

ASパターン(AS Patterns)

ASパターン(AS Patterns)は、caseで一致した際に、asで指定した変数に対象を束縛することができるパターンです。

以下は、シーケンスパターンとクラスパターンを組み合わせつつ、変数にクラスのインスタンスを束縛する例となっています。

from dataclasses import dataclass


@dataclass
class Point:
    """位置クラス"""

    x: int
    y: int


def show(points):
    match points:
        case (Point() as p1, Point() as p2):
            print(p1)
            print(p2)
        case _:
            print("対象外")


if __name__ == "__main__":
    target = [Point(0, 0), Point(10, 10)]
    show(target)

    print("---")
    target = Point(20, 20)
    show(target)
【実行結果】
Point(x=0, y=0)
Point(x=10, y=10)
---
対象外

上記例では、「case (Point() as p1, Point() as p2):」の部分で、Pointクラスのインスタンスを2つ含むリストの一致を確認することができます。一致した場合には、リストの要素をそれぞれp1, p2に束縛します。その後のprintで、p1p2を表示できていることが分かるかと思います。

ORパターン(OR Patterns)

ORパターン(OR Patterns)は、caseの中で複数条件のORで一致をさせることができるパターンで、「|」で条件をつなげることができます。

分かりやすい一つの例としては、以下のようにHTTPレスポンスのパターンを振り分けるような例かと思います。

def check_status(status):
    match status:
        case 200:
            print("OK")
        case 500 | 501 | 502:
            print("サーバーエラー")
        case _:
            print("対象外")


if __name__ == "__main__":
    response = 200
    check_status(response)

    print("---")
    response = 500
    check_status(response)

    print("---")
    response = 501
    check_status(response)

    print("---")
    response = 502
    check_status(response)

    print("---")
    response = 400
    check_status(response)
【実行結果】
OK
---
サーバーエラー
---
サーバーエラー
---
サーバーエラー
---
対象外

HTTPレスポンスでは、200はリクエストが成功したことを表します。一方で、500, 501, …といった500番台はサーバーエラーに関するレスポンスです。

サーバーエラーはまとめて同様の処理としたいような場合等には、「case 500 | 501 | 502:」といったように列挙できます。この例はリテラルパターンとORパターンの組み合わせということになりますが、他のパターンとORパターンを組み合わせももちろん可能です。

まとめ

Python 3.10で追加された構造的パターンマッチ(match caseの使い方について解説しました。

構造的パターンマッチは、その名の通り評価対象がパターンにマッチするかをチェックして、一致した場合の挙動を定義することができます。PEP634では「XXX Patterns」というようにいくつかのパターンが定義されており、本記事では主要なパターンに関して例を使って紹介しました。

構造的パターンマッチを使わなくてもif文で条件分岐を書くことはもちろんできますが、構造的パターンマッチのmatch文をうまく使いこなせると非常に可読性高くコードが書ける可能性があります。

しかしながら、上記で触れたように構造的パターンマッチには多岐にわたる使い方が存在し、比較的複雑な側面があります。そのため、十分な理解なしに使用すると、意図しない挙動やバグを引き起こすリスクも考えられます。使用する際には、システムの要件や状況を十分に検討し、適切に利用することが望ましいです。

より詳細な情報や背景については、構造的パターンマッチの主なPEP文書であるPEP634, 635, 636の文書を参照してください。

Note

構造的パターンマッチの公式ドキュメントはこちらを参照してください。