構造的パターンマッチ

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

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

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

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

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

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

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

構造的パターンマッチには多くのパターンが PEP 634 で定義されています。本記事では、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 の構造的パターンマッチのパターンの種類としては多くの種類があり、PEP 634 には「~ Patterns」というように記載があります。以降では、代表的なパターンについて簡単な例を使って紹介してみたいと思います。

Pythonでは、予約語(keywords)とソフトキーワード(soft keywords)が区別されています。

ソフトキーワードは特定の文脈でのみ特別な意味を持ち、それ以外では通常の識別子として使用できます。

例えば、matchcase は、構造的パターンマッチング(match-case 構文)でのみ特別な意味を持ち、それ以外の文脈では、通常の変数や関数名として使用できます。

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

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 であれば成功、その他の場合は不適切な値として処理されます。

キャプチャパターン(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 つ含むリストの一致を確認することができます。一致した場合には、リストの要素をそれぞれ p1p2 に束縛します。その後の 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の使い方について解説しました。

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

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

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

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

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

ソースコード

上記で紹介しているソースコードについては GitHub にて公開しています。参考にしていただければと思います。

あわせて読みたい
【Python Tech】プログラミングガイド
【Python Tech】プログラミングガイド
ABOUT ME
ホッシー
ホッシー
システムエンジニア
はじめまして。当サイトをご覧いただきありがとうございます。 私は製造業のメーカーで、DX推進や業務システムの設計・開発・導入を担当しているシステムエンジニアです。これまでに転職も経験しており、以前は大手電機メーカーでシステム開発に携わっていました。

プログラミング言語はこれまでC、C++、JAVA等を扱ってきましたが、最近では特に機械学習等の分析でも注目されているPythonについてとても興味をもって取り組んでいます。これまでの経験をもとに、Pythonに興味を持つ方のお役に立てるような情報を発信していきたいと思います。どうぞよろしくお願いいたします。

※キャラクターデザイン:ゼイルン様
記事URLをコピーしました