はじめに
Pythonで条件分岐を行う際、if
、elif
、else
を用いますが、match文も存在しますよね。自分でプログラミングを学ぶ中で、このmatchの使いどころが良く分からなかったので、少し調べてみました。
match文とは
構造的パターンマッチング(structual pattern matching)を実装するための構文で、Python3.10からある機能らしいです。match文で書けるものは、条件をしっかり指定すれば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 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)を使用することで、同じ処理をしたいものを並べられます。(リテラルパターンに限らず使えます)
ちなみに一致するかどうかの判定ですが、数値や文字列は==
を、True
、 False
、 None
などのシングルトンは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文を用いることで、より簡潔に記述できる時がある。
最後に
ここまで読んで下さりありがとうございました。意外と機能が多くて調べるのがちょっと大変でしたが、結構使える場面がありそうだったので、どこかで役立てようと思います。
参考