Python matchステートメントとバイトコード

Google検索が劣化し、次の検索結果を表示していますを抑制するnfpr=1
含まれないを無効化するっぽいnirf=1を駆使してもワード分割が酷くなり
Googleまでもがクォートを案内するが、完全一致は複数形やスペースなど表現の揺らぎさえ消える
結果をまとめサイトが跋扈する昨今、別コミュニティでノウハウを残したい

という事でネタは色々あるのだけど、久しぶりにPythonコードを書いていて
あー任天堂switchしたいと思いきや、いつの間にか実装されてたので検証

# coding=mbcs
from __future__ import absolute_import, division, print_function
import dis

def pattern(var1):
    match var1:
        case 100:
            print("百")
        case 1000:
            print("千")
        case _:
            print("その他")

dis.dis(pattern)

しかし1行目はダメな例として書いた。coding=mbcsは通るがWindows専用で
システムロケール(CP_ACP)を指すため、別環境ではスクリプトと整合性を失う
実行環境に即したファイル読み書きなどを行うときに使うエンコーディング
utf-8と指定すべき所、ダメ文字を抱えていてもcp932が便利な場合も有る

2行目はmatch文を使う以上関係のない、言わずもがな2系で動かすお呪いだが
unicode_literalsは非推奨という点で記述した、代表的にはr"\u"が転ける
RAW文字列リテラルunicodeになると2系はUnicodeエスケープが有効になり
3系はuも付くと忽ち文法エラーになるが、ファイルパス等にbrは宜しくない
またsys.stdoutasciiな場合とかを筆頭に2系はbytesファーストなので
unicode_literalsimportせず、適宜ubを接頭しておくのが良さげ?

バイトコード

閑話休題match文を知るには言語リファレンスを読むのが正攻法ではあるが
実際どういう動きをするかは個人的にバイトコードを調べるのが手っ取り早い

  6           0 LOAD_FAST                0 (var1)     # var1

  7           2 DUP_TOP                               # var1 var1
              4 LOAD_CONST               1 (100)      # var1 var1 100
              6 COMPARE_OP               2 (==)       # var1 var1==100
              8 POP_JUMP_IF_FALSE       12 (to 24)    # var1
             10 POP_TOP

  8          12 LOAD_GLOBAL              0 (print)    # print
             14 LOAD_CONST               2 ('百')     # print '百'
             16 CALL_FUNCTION            1            # None
             18 POP_TOP
             20 LOAD_CONST               0 (None)     # None
             22 RETURN_VALUE

  9     >>   24 LOAD_CONST               3 (1000)     # var1 1000
             26 COMPARE_OP               2 (==)       # var1==1000
             28 POP_JUMP_IF_FALSE       21 (to 42)

 10          30 LOAD_GLOBAL              0 (print)    # print
             32 LOAD_CONST               4 ('千')     # print '千'
             34 CALL_FUNCTION            1            # None
             36 POP_TOP
             38 LOAD_CONST               0 (None)     # None
             40 RETURN_VALUE

 11     >>   42 NOP

 12          44 LOAD_GLOBAL              0 (print)    # print
             46 LOAD_CONST               5 ('その他') # print 'その他'
             48 CALL_FUNCTION            1            # None
             50 POP_TOP
             52 LOAD_CONST               0 (None)     # None
             54 RETURN_VALUE

コメントは筆者によるスタックの変遷を示す。おわかりいただけるだろうか?
スタックの複製を除けば、if-elif-else文とほとんど変わらないのである

  6           0 LOAD_FAST                0 (var1)
              2 LOAD_CONST               1 (100)
              4 COMPARE_OP               2 (==)
              6 POP_JUMP_IF_FALSE       10 (to 20)

  7           8 LOAD_GLOBAL              0 (print)
             10 LOAD_CONST               2 ('百')
             12 CALL_FUNCTION            1
             14 POP_TOP
             16 LOAD_CONST               0 (None)
             18 RETURN_VALUE

  8     >>   20 LOAD_FAST                0 (var1)
             22 LOAD_CONST               3 (1000)
             24 COMPARE_OP               2 (==)
             26 POP_JUMP_IF_FALSE       20 (to 40)

  9          28 LOAD_GLOBAL              0 (print)
             30 LOAD_CONST               4 ('千')
             32 CALL_FUNCTION            1
             34 POP_TOP
             36 LOAD_CONST               0 (None)
             38 RETURN_VALUE

 11     >>   40 LOAD_GLOBAL              0 (print)
             42 LOAD_CONST               5 ('その他')
             44 CALL_FUNCTION            1
             46 POP_TOP
             48 LOAD_CONST               0 (None)
             50 RETURN_VALUE

LOAD_FASTDUP_TOPに成って得られるパフォーマンスは乏しいだろう
しかしmatch文はswitch文に非ず、構造的パターンマッチであるという
何れにも該当しなかった節がdefaultelseでないのもソレ故とScala

シーケンスパターン

立ち返って言語リファレンスを見やる、そしてサンプルを逆アッセンボー

  6           0 LOAD_CONST               1 ((100, 200))

  7           2 DUP_TOP
              4 MATCH_SEQUENCE
              6 POP_JUMP_IF_FALSE       22 (to 44)
              8 GET_LEN
             10 LOAD_CONST               2 (2)
             12 COMPARE_OP               2 (==)
             14 POP_JUMP_IF_FALSE       22 (to 44)
             16 UNPACK_SEQUENCE          2
             18 LOAD_CONST               3 (100)
             20 COMPARE_OP               2 (==)
             22 POP_JUMP_IF_FALSE       22 (to 44)
             24 LOAD_CONST               4 (300)
             26 COMPARE_OP               2 (==)
             28 POP_JUMP_IF_FALSE       23 (to 46)
             30 POP_TOP

  8          32 LOAD_GLOBAL              0 (print)
             34 LOAD_CONST               5 ('Case 1')
             36 CALL_FUNCTION            1
             38 POP_TOP
             40 LOAD_CONST               0 (None)
             42 RETURN_VALUE

  7     >>   44 POP_TOP

  9     >>   46 DUP_TOP
             48 MATCH_SEQUENCE
             50 POP_JUMP_IF_FALSE       46 (to 92)
             52 GET_LEN
             54 LOAD_CONST               2 (2)
             56 COMPARE_OP               2 (==)
             58 POP_JUMP_IF_FALSE       46 (to 92)
             60 UNPACK_SEQUENCE          2
             62 LOAD_CONST               3 (100)
             64 COMPARE_OP               2 (==)
             66 POP_JUMP_IF_FALSE       46 (to 92)
             68 LOAD_CONST               6 (200)
             70 COMPARE_OP               2 (==)
             72 POP_JUMP_IF_FALSE       47 (to 94)
             74 LOAD_FAST                0 (flag)
             76 POP_JUMP_IF_FALSE       47 (to 94)
             78 POP_TOP

 10          80 LOAD_GLOBAL              0 (print)
             82 LOAD_CONST               7 ('Case 2')
             84 CALL_FUNCTION            1
             86 POP_TOP
             88 LOAD_CONST               0 (None)
             90 RETURN_VALUE

  9     >>   92 POP_TOP

 11     >>   94 MATCH_SEQUENCE
             96 POP_JUMP_IF_FALSE       67 (to 134)
             98 GET_LEN
            100 LOAD_CONST               2 (2)
            102 COMPARE_OP               2 (==)
            104 POP_JUMP_IF_FALSE       67 (to 134)
            106 UNPACK_SEQUENCE          2
            108 LOAD_CONST               3 (100)
            110 COMPARE_OP               2 (==)
            112 POP_JUMP_IF_FALSE       67 (to 134)
            114 STORE_FAST               1 (y) # <- !

 12         116 LOAD_GLOBAL              0 (print)
            118 LOAD_CONST               8 ('Case 3, y: ')
            120 LOAD_FAST                1 (y)
            122 FORMAT_VALUE             0
            124 BUILD_STRING             2
            126 CALL_FUNCTION            1
            128 POP_TOP
            130 LOAD_CONST               0 (None)
            132 RETURN_VALUE

 11     >>  134 POP_TOP

 13         136 NOP

 14         138 LOAD_GLOBAL              0 (print)
            140 LOAD_CONST               9 ('Case 4, I match anything!')
            142 CALL_FUNCTION            1
            144 POP_TOP
            146 LOAD_CONST               0 (None)
            148 RETURN_VALUE

おっと…見慣れない命令MATCH_SEQUENCEの出現だ、がドキュメント曰く

If TOS is an instance of collections.abc.Sequence
and is not an instance of str/bytes/bytearray
(or, more technically: if it has the Py_TPFLAGS_SEQUENCE flag set in its tp_flags),
push True onto the stack. Otherwise, push False.

これは後述する特殊化を含めた判定に過ぎず、それ以降は要素毎の比較だ
とすれば真価の発揮は専用命令があるMATCH_KEYSMATCH_CLASSなのか

しかし今回それは割愛、先の特殊化判定。私は行内容の判別を書いていた
それでmatch文に巧く書き直せないかと思ったのだが…リファレンス曰く

If the subject value is an instance of str, bytes or bytearray the sequence pattern fails.

These classes accept a single positional argument,
and the pattern there is matched against the whole object rather than an attribute.

つまりbytearrayでさえシーケンス扱いには成らず、部分一致も不可
memoryviewやアンパックすれば可能だが、そんな事よりin演算子
位置で決まればスライス、又はstartswith()find()した方が良い
split()で扱える場合でも、行頭判別で特定行のみ処理なら非効率だ
論拠Erlangへの言及はある、バイナリパターンマッチもしたいです
そんな訳で私のユースケースインパクトを与える代物ではなかった

言語リファレンス

とはいえ冗長な記述を推敲できる糖衣構文だ、但し要注意なのが文法
matchに与えるsubject_exprstar_named_expression等とされ
これは式のリストに近しく見えるが、case節へ書くのはpatterns
間違っても式expressionではなく、switchRubywhen節と違う

言語リファレンスのサンプルを逆アセンブルした先の結果も示すとおり
コード中に出てくるcase節のyへはSTORE_FAST、つまり代入している
ここへ書いた変数は参照元ではなく代入先で、capture_patternになる
関数オーバーロードにおける仮引数と考えればシグネチャの様な感じか
参照元value_patternとなるケースは、メンバー参照のみとなっている

またNoneTrueFalseも特殊でliteral_patternに分類されるが
この定数というかシングルトン、キーワードにはis演算子が使われる

match True:
    case 1 as x:
        print(x)
  1           0 LOAD_CONST               0 (True)

  2           2 DUP_TOP
              4 LOAD_CONST               1 (1)
              6 COMPARE_OP               2 (==)
              8 POP_JUMP_IF_FALSE       11 (to 22)
             10 STORE_NAME               0 (x)

  3          12 LOAD_NAME                1 (print)
             14 LOAD_NAME                0 (x)
             16 CALL_FUNCTION            1
             18 POP_TOP
             20 JUMP_FORWARD             1 (to 24)

  2     >>   22 POP_TOP

という場合にはマッチしても、下記だとマッチしないという仕様である

match 1:
    case True as x:
        print(x)
  1           0 LOAD_CONST               0 (1)

  2           2 DUP_TOP
              4 LOAD_CONST               1 (True)
              6 IS_OP                    0
              8 POP_JUMP_IF_FALSE       11 (to 22)
             10 STORE_NAME               0 (x)

  3          12 LOAD_NAME                1 (print)
             14 LOAD_NAME                0 (x)
             16 CALL_FUNCTION            1
             18 POP_TOP
             20 JUMP_FORWARD             1 (to 24)

  2     >>   22 POP_TOP

流れ落ちずbreakもなくor_patternを書くが、これも|演算子ではない
guardの後置ifは内包表記でas_patternwithexceptでお馴染み
wildcard_patterncapture_patternの破棄するバージョンなだけだ

所懐

switchしたいと思って出逢ったmatchだったが、ちょっと複雑だった
Elixirのパターンマッチングも鑑みれば、些かPythonicさに欠けるか?
これを否定する気は無いが、3.10~という新しさは時間が解決しても
Windowsだと7が期限切れとはいえ、3.9以降をインストールできない
api-ms-win-core-path-l1-1-0.dllの関数参照は数個で代替可能…

10へと半ば強制更新したMicrosoftの暴挙は環境均一化に寄与したが
AF_UNIXを実装してもSOCK_DGRAMが欠けるからかPythonは未対応
Python3の肝はやはりasyncioだと思うが、これもかなり制限がある
セイウチ演算子が導入され、7でも動く3.8分水嶺的な魅力があり
徒にmatch文を使うのは躊躇われる。だがエラー改良は素晴らしい!!