rcmdnk's blog

そろそろ常識? マンガでわかる「正規表現」 (マンガ×チャットスタイル解説書)

Python 3.10から導入された構造的パターンマッチ(match-case文)で正規表現を使った文字列の 評価を行う方法。

Pythonのmatch-case文

Python 3.10からmatch-caseを使った構造的パターンマッチ(match-case文)が導入されました。

1
2
3
4
5
6
7
8
9
10
x = 2
match x:
    case 1:
        print('one')
    case 2:
        print('two')
    case 3:
        print('three')
    case _:
        print('others')

みたいな感じでmatchのあとに来たものをcaseの各行で評価して該当してたらその中のものを実行します。

最後の_はシェルスクリプトのcase文の*みたいなものですべての状態に合致するため、 最後に持ってくるとC++のdefaultみたいな感じで全部が該当しなかった場合に実行されるものになります。

複数の値でorでみたいときは|

1
2
3
4
5
6
7
8
x = 4
match x:
    case 1|2:
        print('one or two')
    case 3|4:
        print('three or four')
    case _:
        print('others')

みたいな感じで複数をチェックできます。

_の代わりに適当な変数を与えると、それも必ず合致するものになり、かつその変数にmatchの値が代入されます。

1
2
3
4
5
6
7
8
x = 3
match x:
    case 1:
        print('one')
    case 2:
        print('two')
    case a:
        print(a)

また、各caseには後ろにif文を書くことができ、これと上の代入の方法を組み合わせると、

1
2
3
4
5
6
7
8
x = 3
match x:
    case x if x < 2:
        print('one')
    case x if x % 2 == 0:
        print('two')
    case x if x == 3:
        print('three')

みたいな感じでより複雑なチェックを行うことが出来ます。 (このくらいだとif-elseの方がむしろシンプルなのでそうすべきな感じですが。)

また、このように変数をcaseのあとに置くとそこにmatchの値が代入されるため、 別の変数との比較、といった場合にはifと組み合わせて書く必要があります。

1
2
3
4
5
6
7
8
9
10
x = 3
a = 1
b = 2
match x:
    case x if x == a:
        print('a')
    case x if x == b:
        print('b')
    case x if x == c:
        print('c')

な感じ。

xには複数の値(式)を置くことも可能で、

1
2
3
4
5
6
7
8
9
match [1, 1.1, 'a']:
    case 1, 2:
        print('12')
    case 1, 2, 3:
        print('123')
    case (1, 1.1, 'a'):
        print('1234')
    case _:
        print("non")

みたいな感じでcaseの方も複数に対応するものを書いて、 その数と型、値が全部合致したものが実行されるようになっています。 (matchのあと、caseのあといずれもlistやtupleになっていても同じように各箇所があってるかどうか、で判断されます。)

他にも色々出来ることはありますが、PEPのtutorialとかを参考に。

正規表現でのパターンマッチ

シェルスクリプトとかだとabc*abcから始まる文字列にマッチしたいさせる正規表現が使えますが、 Pythonの構造的パターンマッチでは同じようには出来ません。

PEP 634の元となった PEP 622Custom matching protocol という項目に

There were ideas for exotic matchers such as IsInstance(), InRange(), RegexMatchingGroup() and so on.

とあるのでそれっぽいものは考えられてたみたいですが導入は見送られたようです。

1
2
3
4
5
6
7
8
9
match x:
    case "\d+":
        print("number")
    case "^abc":
        print("abc~")
    case ".*xyz.*":
        print("include xyz")
    case _:
        print("non")

みたいな感じで正規表現を書いてそれにマッチしてくれると嬉しいところですが これだとcaseのあとのものはそれぞれの文字がそのまま評価されるので123とかはマッチしません。

直接的にそのまま正規表現をcaseに渡すことは出来ませんが、 評価するものは文字列以外のものでも良いし、 match渡されたものを直接if文で評価することも可能なので色々やる方法はありそうです。

ifでチェックする

1
2
3
4
5
6
7
8
9
10
11
12
13
import re


x="123"
match x:
    case a if re.search("\d+", a) is not None:
        print("number")
    case a if re.search("^abc", a) is not None:
        print("abc~")
    case a if re.search(".*xyz.*", a) is not None:
        print("include xyz")
    case _:
        print("non")

みたいな感じにすればnumberになります。

ただこれだとif-elseで書いたほうがむしろシンプル。

__eq__で正規表現matchを行う

caseではmatchで与えられたものとそこに書かれたものが==(__eq__) で評価されているだけです。

なので文字列を正規表現で評価したいなら文字クラスを継承して __eq__の部分で正規表現との比較を出来るようにしてあげればよい、という方法が 現状では一番スマートに見える解になっているようです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import re


class StrRe(str):
    def __init__(self, var):
        self.var = var
        pass

    def __eq__(self, pattern):
        return True if re.search(pattern, self.var) is not None else False


x="123"
match StrRe(x):
    case "\d+":
        print("number")
    case "^abc":
        print("abc~")
    case ".*xyz.*":
        print("include xyz")
    case _:
        print("non")

Ref: Recipes and Tricks for Effective Structural Pattern Matching in Python Martin Heinz

こんな感じのStrReというstrを継承して__eq__だけ正規表現でマッチするように変更したクラスを作って そのオブジェクトを作ってmatchにわたすことでcase側には正規表現な文字列を渡すだけでチェックできるようになってます。

これだとmatch-case文を使って書いたことでスッキリした感じが強いです。

ライブラリを使う

似たようなものですがライブラリとして公開されているものがあります。

これを

$ pip install regex-spm

でインストールして、

1
2
3
4
5
6
7
8
9
10
11
12
13
import regex_spm


x="123"
match regex_spm.search_in(x):
    case "\d+":
        print('number')
    case "^abc":
        print("abc~")
    case ".*xyz.*":
        print("include xyz")
    case _:
        print("non")

みたいな感じで使います。

使えるのはsearch_inmatch_infullmatch_in でそれぞれre.searchre.matchre.fullmatchに対応しています。

  • search_in: 部分マッチ
  • match_in: 先頭からの部分マッチ
  • fullmatch_in: 完全マッチ

なので、

  • x="123": 全部でnumber
  • x="123A": search_inmatch_innumber
  • x="A123": search_inのみnumber

となります。

regex_spmは上のStrReに比べてちょっと入力側で色々出来るようになっていて、 中で正規表現側を一旦re.compileしてますが、その第二引数のflagを渡すようにtupleで比較することも出来るようになってます。

regex_spm/regex_spm_match.py at main · aronhoff/regex_spm

ちょっとした環境でなら自分でクラスを作ってしまってもそれほど大したことないですが、 色々とライブラリを導入するような環境ならregex_spmを入れて使っても良いかな、と思います。

Sponsored Links
Sponsored Links

« シェルスクリプトでちゃんと文字列に改行を入れ込む GitHub Actionsで行ったtestのcoverageの結果を別ブランチにpushする »

}