3
1

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】match文について調べてみた

Posted at

はじめに

 Pythonで条件分岐を行う際、ifelifelseを用いますが、match文も存在しますよね。自分でプログラミングを学ぶ中で、このmatchの使いどころが良く分からなかったので、少し調べてみました。

match文とは

 構造的パターンマッチング(structual pattern matching)を実装するための構文で、Python3.10からある機能らしいです。match文で書けるものは、条件をしっかり指定すればif文でも実装可能ですが、以下のような場合に、簡潔に記述するための機能っぽいです。

if
if isinstance(x, tuple) and len(x) == 2:
   host, port = x
   mode = "http"
elif isinstance(x, tuple) and len(x) == 3:
    host, port, mode = x
# Etc.
match
match x:
    case host, port:
        mode = "http"
    case host, port, mode:
        pass
    # Etc.

たしかに長さで条件分岐する必要がなく、分かりやすい気がします。

C言語などのswitch-caseにも似ていますが、あちらは条件に合致したcase以降のcaseの処理もすべて行われる(breakしなければ)のに対し、こちらはマッチした部分のみが実行されるなど違いがあります。

いろいろな書き方

  • リテラルパターン(literal_pattern)
  • キャプチャーパターン(capture_pattern)
  • ワイルドカードパターン(wildcard_pattern)
  • バリューパターン(value_pattern)
  • グループパターン(group_pattern)
  • シーケンスパターン(sequence_pattern)
  • マッピングパターン(mapping_pattern)
  • クラスパターン(class_pattern)

上記のようにいろいろなマッチングのパターンがあるようなので、一つずつ説明します。

リテラルパターン

x = 3
match x:
    case 1:
        print('1です')
    case 2:
        print('2です')
    case 3:
        print('3です')

一番シンプルな書き方です。単純に上から見ていって、マッチしたcaseの処理が実行されます。

x = 4
match x:
    case 1 as number:
        print('1です')
    case 2 as number:
        print('2です')
    case 3 | 4 as number:
        print('3か4です')
print(number)

asを使うことで、マッチしたときの値を変数に代入することもできます。また、|(or)を使用することで、同じ処理をしたいものを並べられます。(リテラルパターンに限らず使えます)

ちなみに一致するかどうかの判定ですが、数値や文字列は==を、TrueFalseNoneなどのシングルトンはisを用いているようです。

キャプチャーパターン

x = 3
match x:
    case 1:
        print('1です')
    case 2:
        print('2です')
    case n:
        print(f'1でも2でもなく{n}です')

一番下のcase nがキャプチャーパターンです。この場合には必ずマッチし、caseの後に宣言された変数へ、値が代入されるようです。キャプチャーパターンはリテラルパターンより後に記述しないと、以下のエラーが出ます。

SyntaxError: name capture 'n' makes remaining patterns unreachable
x = 8
match x:
    case 1:
        print('1です')
    case 2:
        print('2です')
    case n if n % 2 == 0:
        print(f'3以上の偶数です')

必ずマッチさせたくない場合には、ifによる条件(ガード)を付けることができます。(キャプチャーパターンに限らず使えます)

x = (1, 2, 3)
match x:
    case a, b:
        print(a, b)
    case a, b, c
        print(a, b, c)

後述するシーケンスパターンに当たりますが、カンマ区切りで同時に複数の値を判定可能です。また、_*を使って、以下のようなこともできます。

x = (1, 2, 3, 4, 5)
match x:
    case a, b:
        print(a, b)
    case a, *b, _, c:
        print(a, b, c)    #1 [2, 3] 5

ただし_には本当に何も代入されません。通常はprint(_)としても値が出力されますが、この場合は定義されていないのでエラーになります。

ワイルドカードパターン

x = 3
match x:
    case 1:
        print('1です')
    case 2:
        print('2です')
    case _:
        print('1でも2でもありません')

キャプチャーパターンとほぼ同じですが、変数名を_にすると、何も受けとらず、処理のみ行えます。したがって、if文でいうところのelseに当たる使い方ができます。他言語のswitch文にあるdefaultともよく似ています。

x = 3
match x:
    case 1:
        print('1です')
    case 2:
        print('2です')
    case _ as number:
        print(f'1でも2でもなく{number}です')

asと組み合わせることで、どのパターンにもマッチしなかった場合に、何の値が渡されたのか簡単に取得できるのは結構便利そうです。

バリューパターン

class MyClass:
    num1 = 1
    num2 = 2
    num3 = 3
x = 2
match x:
    case MyClass.num1:
        print(f'{MyClass.num1}です')
    case MyClass.num2:
        print(f'{MyClass.num2}です')
    case MyClass.num3:
        print(f'{MyClass.num3}です')

書き方としてはキャプチャーパターンと同じなのですが、ドットアクセスする変数名を置いておくと、代入ではなくマッチするか判定してくれます。つまり、リテラルパターンのように動いてくれます。

グループパターン

x = 3
match x:
    case (1 | 2):
        print('1か2です')
    case (3 | 4):
        print('3か4です')
    case (5 | 6):
        print('5か6です')

わざわざ項目を作るか迷ったのですが、カッコで囲むだけです。効果も見やすくなるだけです。公式にも、意図するグループを強調するとしか書かれていません。

シーケンスパターン

x = (1, 3)
match x:
    case (1, 1):
        print('1, 1です')
    case (1, 2):
        print('1, 2です')
    case (1, 3):
        print('1, 3です')

上の例はリテラルパターンのリストやタプル版といった感じの使い方です。ただし、'"のように([を区別はしないみたいです。また、カッコ無しでも動きました。

x = (1, 3)
match x:
    case (1, n):
        print(f'1, {n}です')
    case (2, n):
        print(f'2, {n}です')
    case (3, n):
        print(f'3, {n}です')

上記のようにキャプチャーパターンと組み合わせられます。これはすごく面白そうな感じがします。

x = list('abcdefg')
match x:
    case ("a", *_, "g"):
        print("aから始まり、gで終わります")
    case ("a", *_):
        print("aから始まります")
    case ("b", *_):
        print("bから始まります")
    case _:
        print("その他から始まります")

上手く組み合わせると、こんな感じでちょっとテクニカルな判定もできます。

マッピングパターン

x = {'バナナ': 'banana', 'ブドウ': 'grape', 'レモン': 'lemon'}
match x:
    case {'リンゴ': 'apple', 'ブドウ': 'grape'} as fruits:
        print(fruits)
    case {'バナナ': 'banana', 'ブドウ': 'grape'}as fruits:
        print(fruits)
    case {'レモン': 'lemon', 'ブドウ': 'grape'}as fruits:
        print(fruits)

リテラルパターンの辞書版といった感じです。keyとvalueのどちらも一致している必要がありますが、余計な要素がついていても一致している判定になります。実際、上の例でも2つ目のcaseが処理されます。逆に足りない要素がある場合は、一致していない判定になります。つまり、部分的にでも一致している個所があれば、一致している判定になると言えます。

x = {'バナナ': 3, 'ブドウ': 6}
match x:
    case {'リンゴ': apple, 'ブドウ': grape}:
        print(f'リンゴ{apple}個、ブドウ{grape}')
    case {'バナナ': banana, 'ブドウ': grape}:
        print(f'バナナ{banana}個、ブドウ{grape}')
    case {'レモン': lemon, 'ブドウ': grape}:
        print(f'レモン{lemon}個、ブドウ{grape}')

キーの方は変数を指定できませんが、バリューには変数を指定できるので、キャプチャーパターンも使えます。

data = {"type": "circle", "radius": 5}

match data:
    case {"type": "circle", "radius": r} if r > 0:
        print(f"円の半径は {r} です")
    case {"type": "rectangle", "width": w, "height": h}:
        print(f"長方形の幅は {w}、高さは {h} です")
    case _:
        print("未知の形状です")

AIが分かりやすいものを書いてくれたので、上手く組み合わせた使用例を置いておきます

クラスパターン

class Person:
    def __init__(self, name, age, job):
        self.name = name
        self.age = age
        self.job = job

x = Person("たかし", 30, "無職")
match x:
    case Person(name = name, age = age, job = "無職"):
        print(f"名前: {name}, 年齢: {age}")

もしくは

class Person:
    __match_args__ = ("name", "age", "job")
    def __init__(self, name, age, job):
        self.name = name
        self.age = age
        self.job = job

x = Person("たかし", 30, "無職")
match x:
    case Person(name, age, "無職"):
        print(f"名前: {name}, 年齢: {age}")

このように書くことができます。インスタンスをパターンマッチングさせるのに使います。インスタンスがそのクラスのものであるか、特定の属性を持つかによって、一致するかを判定してくれます。if文でisinstanceを使って判定している部分を置き替えるのがよさそうです。

ちなみにですが、case Person(name, age, "無職")の行は、新しいインスタンスが作成されていそうな見た目をしていますが、そういうわけではないみたいです。Person__new__を以下のようにすると確認できます。

class Person:
    __match_args__ = ("name", "age", "job")
    
    @classmethod
    def __new__(cls, *args, **kwargs):
        print('新しいインスタンスが作成されました!')
        return super().__new__(cls)

    def __init__(self, name, age, job):
        self.name = name
        self.age = age
        self.job = job

x = Person("たかし", 30, "無職")
match x:
    case Person("たかし", 25, "無職"):
        print('名前: たかし, 年齢: 25')
    case Person(name, age, "無職"):
        print(f"名前: {name}, 年齢: {age}")

おまけ:namedtuple

 namedtupleの場合、クラスパターンとシーケンスパターンの両方にマッチします。以下の例の順番を入れ替えて、試していただければ確認できます。

from collections import namedtuple

Person = namedtuple("Person", ["name", "age", "job"])
x = Person("たかし", 30, "無職")
match x:
    case Person(name, age, "無職"):
        print(f"名前: {name}, 年齢: {age}:クラスパターンにマッチしました")
    case (name, age, "無職"):
        print(f"名前: {name}, 年齢: {age}:シーケンスパターンにマッチしました")

また、namedtupleの場合は、__match_args__が自動設定されるようです。

おまけ:自作のシングルトン

流石に自作のシングルトンは==で比較されるようでした。

class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __bool__(self):
        return False

    def __eq__(self, other):
        return not bool(other)

singleton = Singleton()
print(singleton == None)    # True
print(singleton is None)    # False

x = None

match x:
    case Singleton._instance:  # `is` による比較が行われないのでこっち
        print("Matched Singleton instance")
    case _:
        print("Not Matched Singleton instance")

x = singleton

match x:
    case None:
        print('Matched None')
    case _:    # `is`による比較が行われるのでこっち
        print("Not Matched None")

ちょっと使ってみた

特定の単語を抜き出す

word_list = [
    "forest", "cave", "mountain", "desert", "ocean", "river", "lake", "swamp",
    "valley", "hill", "plain", "island", "cliff", "canyon", "gorge", "bay",
    "peninsula", "plateau", "tundra", "savanna"
]
for w in word_list: 
    match list(w):
        case [*_, 'e', _]:
            print(w)

別にこのくらいならif文でもできますが、似たような感じで、特定の単語で終わる文を抜き出すような場合など、何かしら応用はできそうです。

式を受け取って計算

def calculator(expression):
    match expression.split():
        case [a, "+", b]:
            return float(a) + float(b)
        case [a, "-", b]:
            return float(a) - float(b)
        case [a, "*", b]:
            return float(a) * float(b)
        case [a, "/", b] if b != "0":
            return float(a) / float(b)
        case _:
            return "無効な式です"

print(calculator("3.5 + 5"))
print(calculator("10 - 2"))
print(calculator("4 * 7"))
print(calculator("8 / 0"))

これもわざわざmatch文でやることではない気がしますが、何か似たような形で活用できそうです。

山札からトランプを引く

import random
card_list = []
for suit in ['ハート', 'スペード', 'ダイヤ', 'クラブ']:
    for number in ['A', 2, 3, 4, 5, 6, 7, 8, 9 ,10, 'J', 'Q', 'K']:
        card_list.append([suit, number])
card_list.append(['Joker'])
x = random.choice(card_list)
match x:
    case [suit, number]:
        print(f"{suit}{number}が出ました")
    case _:
        print("ジョーカーが出ました")

これは結構すっきりかけた印象ですね。

まとめ

  • 条件が複雑で、ネストが深くなりやすい場合
  • パターンが明確で一目で処理内容を理解したい場合

このような場合には、if文の代わりにmatch文を用いることで、より簡潔に記述できる時がある。

最後に

 ここまで読んで下さりありがとうございました。意外と機能が多くて調べるのがちょっと大変でしたが、結構使える場面がありそうだったので、どこかで役立てようと思います。

参考

3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?