0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[中級者向け] Python 構造的パータンマッチの 10 のパターン

Last updated at Posted at 2025-04-22

はじめに

比較的新しい機能である構造的パターンマッチ文 (以下、マッチ文) を解説します。
初学者はまず if 文で条件分岐を身に付けるべきと考え、タイトルでは中級者向けと謳っています。

Python 3.10 以降の機能です

マッチ文の概要

他言語における switch 文のようなものです。

if 文での条件分岐
a = 0

if a == 0:
    print("0")
elif a == 1:
    print("1")
else:
    print("other")

--> 0
マッチ文での条件分岐
a = 0

match a:
    case 0:
        print("0")
    case 1:
        print("1")
    case _:
        print("other")

--> 0

マッチ文を定義する手順

  1. match の後ろに値を記述して、: で区切る
  2. case の後ろに値を記述して、: で区切る
  3. 分岐の数だけ 2. を繰り返す
  4. どの条件にも当てはまらない処理は case _: の後に記述

Java の switch 文では分岐が多いと記述が簡潔になるとのことですが、Python はもとより if 文の記述が簡潔なため、マッチ文のメリットが薄いように思えます。

ですが以下のケースに当てはまる状況下では、その恩恵を享受できます。

OR パターン

| で要素を列挙するパターンです。

複数のパターンを併記
mob = "villager"

match mob:
    case "zombie" | "skeleton" | "creeper":
        print("Hostile")
    case "chicken" | "pig" | "villager":
        print("Passive")
    case "enderman" | "piglin":
        print("Neutral")

--> Passive

if 文でいうところの or で繋げるイメージです。

AS パターン

as で条件を満足した要素と変数を束縛するパターンです。

変数への束縛
mob = "villager"

match mob:
    case "zombie" | "skeleton" | "creeper" as hostile:
        print(hostile)
    case "chicken" | "pig" | "villager" as passive:
        print(passive)
    case "enderman" | "piglin" as neutral:
        print(neutral)

--> villager

ビルトインクラストのマッチは こちら を参照してください。

リテラルパターン

リテラルを利用した一般的な例です。
内部的には == (真偽値や Noneis) で評価しています。

だいたいのリテラルが利用できる
val = None

match val:
    case 0:
        print(0)
    case "abc":
        print("abc")
    case """abcabc""":
        print("abcabc")
    case r"\t\n":
        print(r"\t\n")
    case b"abc":
        print("byte-abc")
    case True:
        print(True)
    case False:
        print(False)
    case None:
        print(None)
    case _:
        print("other")

--> None

数値 (複素数)・文字列・真偽値・None が利用できますが、式と f 文字列は記述できません。

式は利用できない
val = 2

match val:
    case 1 + 1:
        print(2)
    case _:
        print("other")

--> SyntaxError

ちなみに複素数の場合のみ、+- を利用できます。
複素数を使うタイミングなどほとんどないとは思いますが...

複素数
val = 1 + 2j

match val:
    case 1 + 2j:
        print("1+2j")
    case 1 - 2j:
        print("1-2j")

--> 1+2j

ちなみに実数は左側で虚数は右側と決まっています。

実数と虚数の位置は変更できない
val = 1 + 2j

match val:
    case 2j + 1:
        print("1+2j")
    case -2j + 1:
        print("1-2j")

--> SyntaxError

キャプチャパターン

パターンに変数を指定することで、常にマッチを成功させ値をその変数に束縛します。

値を変数に束縛
mob = "slime"

match mob:
    case name:
        print(name)

--> slime

後述するパターンと組み合わせることで、真価を発揮します。

ワイルドカードパターン

前述した _ のことです。
最後に記述することで、else のように振舞います。

値を変数に束縛しない
mob = "slime"

match mob:
    case _:
        print(_)

注意点はキャプチャパターンより優先されるため、_ に値が束縛されないことぐらいでしょうか。
つまり上では case __mob は紐づいていませんし、そもそも case _print(_)_ は別物なので、なにも表示されません。

バリューパターン

オブジェクトの属性と値を比較します。
. が付いていると、バリューパターンと認識されます。

オブジェクトの属性と比較
class Pokemon:
    def __init__(self, name):
        self.name = name


pikachu = Pokemon("Pikachu")

name = "Pikachu"

match name:
    case pikachu.name:
        print(name)

--> Pikachu

グループパターン

() でパターンの可読性を高めるだけです。

長いパターン
tool = "chainsaw"

match tool:
    case "auger" | "chainsaw" | "impact driver":
        print(tool)

--> chainsaw
グルーピングしたパターン
match tool:
    case ("auger" | "chainsaw" | "impact driver"):
        print(tool)

--> chainsaw

この程度の構文であれば、可読性に差は感じません。
AS パターンを併用した例で確認すると、効果を実感できそうです。

グルーピングで可読性を向上
mob = "villager"

match mob:
    case ("zombie" | "skeleton" | "creeper") as hostile:
        print(hostile)
    case ("chicken" | "pig" | "villager") as passive:
        print(passive)
    case ("enderman" | "piglin") as neutral:
        print(neutral)

シーケンスパターン

リストやタプルといったシーケンス系にマッチするパターンです。

単純なリストのマッチ
lst_1 = [0, 1, 2]

match lst_1:
    case [0, 1, 2]:
        print(lst_1)

--> [0, 1, 2]
単純なタプルのマッチ
tup_1 = (0, 1, 2)

match tup_1:
    case (0, 1, 2):
        print(tup_1)

--> (0, 1, 2)

これでは if 文で事足りますが、下のようなケースではかなり簡潔に記述できます。

要素数が 2 で 0 から始まるリスト
lst_2 = [0, 1]

match lst_2:
    case [0, x]:
        print(x)

--> 1

[0, x]0 から始まる 2 要素のリストであれば、次の要素を x に束縛するという意味です。
キャプチャパターンとの合わせ技になっているのがミソです。

上を if で
lst_2 = [0, 1]

if len(lst_2) == 2 and lst_2[0] == 0:
    print(lst_2[1])

--> 1

どうでしょう、マッチ文のほうがかなり読みやすくはないでしょうか?
要素を増やしたもう少し複雑な例も確認します。

要素数が 3 で 0, 1 から始まるリスト
lst_1 = [0, 1, 2]

match lst_1:
    case [0, 1, x]:
        print(x)

--> 2
上を if で
lst_1 = [0, 1, 2]

if len(lst_1) == 3:
    if lst_1[0] == 0 and lst_1[1] == 1:
        print(lst_1[2])

--> 2

このレベルだと if 文の可読性は酷いものです。

if 文と len() を同時に利用する場合は、ぜひマッチ文も検討してください

複数の変数

パターン内で複数の変数を利用する際には、別の変数を記述してください。

複数の変数
lst_1 = [0, 1, 2]

match lst_1:
    case [0, x, y]:
        print(x)
        print(y)

--> 1
    2

可変長の要素

関数定義と同様に、要素が可変長であれば * を記述します。

可変長の要素
lst_1 = [0, 1, 2]

match lst_1:
    case [0, *values]:
        for value in values:
            print(value)

--> 1
    2

マッピングパターン

ディクショナリのようなマッピング系にマッチするパターンです。

単純なディクショナリのマッチング
mob = {"name": "ghast", "health": 10, "width": 4.0}

match mob:
    case {"name": "ghast"}:
        print(mob)

--> {"name": "ghast", "health": 10, "width": 4.0}

指定した要素が含まれるかのみで判断するため、他の要素が存在してもマッチすることは注意してください。
AS パターンとの合わせ技で、下のような型の判定のような記述も可能となります。

値の型を判定
mob = {"name": "ghast", "health": 10, "width": 4.0}

match mob:
    case {"name": str() as name}:
        print(name)

--> ghast

str() のようなビルトイン関数は下のようにも記述できます。

ビルトイン関数特有の記述
mob = {"name": "ghast", "health": 10, "width": 4.0}

match mob:
    case {"name": str(name)}:
        print(name)

--> ghast
上を if で
mob = {"name": "ghast", "health": 10, "width": 4.0}

if "name" in mob and isinstance(mob["name"], str):
    print(mob["name"])

シーケンスパターンほどの差はないですが、マッチ文だと意図が明瞭にみえます。

if 文と isinstance() を同時に利用する場合は、ぜひマッチ文も検討してください

他の要素をキャプチャ

関数定義のキーワード可変長引数と似ていますが、** を記述するだけです。

パターン以外の要素を取得
mob = {"name": "ghast", "health": 10, "width": 4.0}

match mob:
    case {"name": str(name), **features}:
        print(features)

--> {"health": 10, "width": 4.0}

クラスパターン

クラス系にマッチするパターンです。
今まで同様にパターン内の変数に値を束縛します。

単純なクラスのマッチング
class Pokemon:
    def __init__(self, name, type, id):
        self.name = name
        self.type = type
        self.id = id


pikachu = Pokemon("Pikachu", "Electric", 25)

match pikachu:
    case Pokemon(name="Pikachu", type=type, id=id):
        print(type)
        print(id)

--> Electric
    25
上を if で
pikachu = Pokemon("Pikachu", "Electric", 25)

if isinstance(pikachu, Pokemon):
    print(pikachu.type)
    print(pikachu.id)

--> Electric
    25

やはりマッチ文のほうが直感的かつ明瞭にみえます。

位置引数によるマッチング

実はデフォルトだと位置引数によるマッチングができません。
対処法は簡単で、クラス定義に __match_args__ を追加するだけです。

パターンに位置引数を適用
class Pokemon:
    def __init__(self, name, type, id):
        self.name = name
        self.type = type
        self.id = id

    __match_args__ = ("name", "type", "id")


pikachu = Pokemon("Pikachu", "Electric", 25)

match pikachu:
    case Pokemon("Pikachu", type, id):
        print(type)
        print(id)

--> Electric
    25

__match_args__ に属性名を文字列としてタプルで渡すだけです。

ガード

case の後ろに if を記述することで、複雑な条件に柔軟に対応できます。

下ではガラル地方のポケモン (id が 810 ~ 898) のみ表示するために、パターンの後に if 文を追加して、制限を設けることでより細かな条件を指定しています。

パターンに条件を追加
class Pokemon:
    def __init__(self, name, type, id):
        self.name = name
        self.type = type
        self.id = id

    __match_args__ = ("name", "type", "id")


pikachu = Pokemon("Pikachu", "Electric", 25)
cinderace = Pokemon("Cinderace", "Fire", 815)


match pikachu:
    case Pokemon(name, type, id) if 810 <= id <= 898:
        print(type)
        print(id)

match cinderace:
    case Pokemon(name, type, id) if 810 <= id <= 898:
        print(type)
        print(id)

--> Fire
    815

エースバーン (英名 : Cinderace) は if 文の条件を満足するため、タイプと図鑑 No. (ここでは id) が表示されます。

まとめ

if 文でも記述はできるので、マッチ文に触れたことない方も多かったかもしれません。
とはいえ前述した 10 のパターンとガードさえおさえれば、マッチ文のスペシャリストといっても過言ではありません。
今後条件分岐を記述する際は、マッチ文と if 文を使い分けてロバストなコーディングを目指してください。

更新履歴

2025/04/23 初版
2025/04/24 改行を調整 & 誤植を修正

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?