Python

【Python】構造パターンマッチングについて

この記事では、Python 3.10で追加されたパターンマッチングについて解説します。

パターンマッチングの構文は、C言語などのswitch文とよく似ていて処理を分岐するところまでは一緒ですが、パターンにマッチするかどうかで分岐が可能です。

つまりパターンマッチングは、値で分岐したい際に使うのではなく、受け取った値の形(パターン)によって処理を分岐させる構文だと言えます。

それでは、実際にパターンマッチングを使いながら使用方法を確認してきましょう!

パターンマッチングの構文

パターンマッチングは、matchcaseを使って表現されます。

match オブジェクトや式:
    case パターン1:
        # パターン1と一致した際に実行される処理
    case パターン2:
        # パターン2と一致した際に実行される処理

matchの右に指定したオブジェクトや式(サブジェクト値)を評価して上から下にcaseのパターンと照合していきます。

マッチングした場合には、そのcase内の処理が実行され、それ以降のcaseは無視されます。

パターン

caseに指定できるパターンには以下のようなものがあります。

  • リテラルパターン
  • キャプチャパターン
  • ワイルドカードパターン
  • バリューパターン
  • グループパターン
  • シーケンスパターン
  • マッピングパターン
  • クラスパターン

リテラルパターン

リテラルを指定することでリテラルパターンとして定義できます。数値と文字列は==演算子を使用して比較され、組み込み定数であるNoneTrueFalseis演算子を用いて比較されます。

num = 2

match num:
    case 1:
        print('1です')
    case 2:
        print('2です')

実行結果

2です

キャプチャパターン

任意の変数名を指定することでキャプチャパターンとして定義できる。キャプチャパターンは常に成功します。

また、マッチングしたオブジェクト(サブジェクト値)を指定した名前にバインドするのでcase内で使用することが可能です。

num = 1

match num:
    case n:
        print(n)

実行結果

1

「常に成功するのにどこで使うの?」と思うかもしれませんがシーケンスパターンなどと組み合わせることでかなり強力なパターンとなります。

注意として1つのパターンの中では同じ名前を使うことはできません。case x, x:というような使い方をするとSyntaxErrorが送出されます。

ワイルドカードパターン

_(アンダーバー)を指定することでワイルドカードパターンとして定義できます。ワイルドカードパターンは、どこにもマッチングしなかった際の処理を定義することができます。

num = 0

match num:
    case 1:
        print('1です')
    case _:
        print('1以外です')

実行結果

1以外です

ワイルドカードパターンでは、サブジェクト値を名前にバインドしません。なので、マッチングはさせたいけどcase本体の処理中でサブジェクト値を使わない場合にも使います。

バリューパターン

.を1つ以上含む名前の値(属性)を指定することでバリューパターンとして定義できます。バリューパターンは、指定された属性の値と同じ値か==演算子で比較されます。

num = 2

n1 = 1
n2 = 2 

match num:
    case n1.real:
        print('1です')
    case n2.real:
        print('2です')

実行結果

2です

.を含まないとキャプチャパターンとして認識されてしまうので注意してください。

グループパターン

グループパターンは、パターンを強調するための構文です。パターンを()で囲って表現します。

num = 2

match num:
    case (1):
        print('1です')
    case (2):
        print('2です')

実行結果

2です

括弧内で複数のパターンを,で区切って指定すると、次で解説するシーケンスパターンとなるので注意してください。

シーケンスパターン

()[]内に複数のパターンを,で区切って指定することでシーケンスパターンとして定義することができます。

num = [2, 4]

match num:
    case [1, 2, 3]:
        print('1, 2, 3です')
    case (2, 4):
        print('2, 4です')

実行結果

2, 4です

キャプチャパターンと組み合わせることでデータ構造でマッチングさせることができます。

num = [1, 3, 5]

match num:
    case [v1, v2, v3]:
        print(f'{v1}, {v2}, {v3}です')
    case (2, 4):
        print('2, 4です')

実行結果

1, 3, 5です

リテラルパターンと組み合わせることで特定の箇所の値のみを固定することができます。

num = [2, 1, 2]

match num:
    case [1, v1, v2]:
        print(f'1: {v1}, {v2}')
    case (2, v1, v2):
        print(f'2: {v1}, {v2}')

実行結果

2: 1, 2

また、ワイルドカードパターンと組み合わせて使うこともできます。

s= [2, 1, 2]

match s:
    case [1, _, _]:
        print(1)
    case (2, _, _):
        print(2)

実行結果

2

マッピングパターン

マッピング型のオブジェクトを指定することでマッピングパターンとして定義できます。

d = {'1': 'one', '2': 'two'}

match d:
    case {'1': 'one', '2': 'two'}:
        print(1)
    case {'one': '1', 'two': '2'}:
        print(2)

実行結果

1

キャプチャパターンなどと組み合わせることもできる。

d = {'name': 'Mike', 'age': 20}

match d:
    case {'name': name, 'age': age}:
        print(f'名前: {name}さん, age: {age}歳')

実行結果

名前: Mikeさん, age: 20歳

クラスパターン

クラスを任意の初期値とともに指定することでクラスパターンとして定義できます。クラスと属性値が同じならマッチングします。

class Person:
    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age

p = Person('Mike', 20)

match p:
    case Person(name='John', age=32):
        print('32歳のJohn')
    case Person(name='Mike', age=20):
        print('20歳のMike')

実行結果

20歳のMike

caseに指定するクラスパターンは、基本的にキーワード呼び出しで値を渡す必要があるが、例外としてクラスに__match_args__を定義することで位置専用で指定することができます。

__match_args__はクラス変数として文字列のタプルで指定できます。

class Person:

    __match_args__ = 'name', 'age'

    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age

p = Person('Mike', 20)

match p:
    case Person('John', 32):
        print('32歳のJohn')
    case Person('Mike', 20):
        print('20歳のMike')

実行結果

20歳のMike

また、dataclassを使った場合は__match_args__も自動生成してくれる。

@dataclass
class Person:
    name: str
    age: int

p = Person('Mike', 20)

match p:
    case Person('John', 32):
        print('32歳のJohn')
    case Person('Mike', 20):
        print('20歳のMike')

実行結果

20歳のMike

特殊なパターン

ASパターン

パターンとして指定したオブジェクトをcaseの本体の処理内で使用したい場合はasで指定した名前にバインドすることができます。

class Person:
    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age

p = Person('Mike', 20)

match p:
    case Person(name='John', age=32) as john:
        print(john.name, john.age)
    case Person(name='Mike', age=20) as mike:
        print(mike.name, mike.age)

実行結果

Mike 20

ORパターン

|演算子を使うことで1つのcaseに複数のパターンを指定することができます。

num = 3

match num:
    case 1 | 2:
        print('1か2です')
    case 3 | 4:
        print('3か4です')

実行結果

3か4です

asと組み合わせることもできます。

num = 3

match num:
    case 1 | 2 as n:
        print(f'{n}です')
    case 3 | 4 as n:
        print(f'{n}です')

実行結果

3です

ガード

パターンの後ろにif文を使って式を指定することでマッチングした際にcase内の処理を実行するかどうかを条件によって分岐することができます。

num = 11

match num:
    case n if n > 10:
        print(f'{n}です')

実行結果

11です

ガードはパターンの一部ではなくcaseの一部です。

まとめ

この記事では、Pythonのパターンマッチングについて解説しました。

パターンマッチングは様々な状況で使用することができますが、雑にパターンを定めているとその柔軟性ゆえに思わぬバグを引き起こす可能性もはらんでいます。

しっかりパターンを精査してから実装しましょう!

それでは今回の内容はここまでです。ではまたどこかで〜( ・∀・)ノ