概要・経緯
想定読者はPython初級者から中級者に上がろうとしている人。
『Effective Python』という本を読んでいた際に、キャプチャパターンと値パターンという言葉が出ており、なんのことかさっぱりわからなかったので調べた。
キャプチャーパターン、値パターンとは
パターンマッチングは、パターン (case の後ろ) とマッチング対象の値 (match の後ろ)を入力とし、パターンとマッチング対象の値がマッチするかどうか判定するものである。
パターンは全部で10種類存在する。
- ORパターン
- ASパターン
- リテラルパターン
- キャプチャパターン
- ワイルドカードパターン
- 値パターン
- グループパターン
- シーケンスパターン
- マッピングパターン
- クラスパターン
今回はキャプチャパターンと値パターンについてみていく。
match 11:
case x:
print(x)
キャプチャーパターンでは、そのマッチ対象(サブジェクト)を変数 x に代入する。サブジェクトとはmatchの右側の値のことである。
そして、キャプチャーパターンの注意として、いつでも成功するということである。
この時、上のコードでは x = 11 なので11が出力される。
そして値パターンでは、定数などすでに定義された値と等しいときにマッチする。
つまり、x == 11 なら print(x) を実行するということだ。
なんのことかよくわからないと思うのでこれから詳しく説明していく。
実際の例
『Effective Python』で実際にこれらの言葉が出たコード例を示す。(全く同じではあれかもしれないので似たような書き方をする別のプログラムに置き換えた)
動物の名前を受け取って、それぞれ違う鳴き方をするプログラムを考える。
def take_action(animal):
if animal == "cat":
print("にゃー")
elif animal == "dog":
print("わん!")
elif animal == "cattle":
print("モー")
else:
raise RuntimeError
これらをパターンマッチングを使用してより可読性が高いプログラムに変更すると以下のようになる。
# 例1
def take_match_action(animal):
match animal:
case "cat":
print("にゃー")
case "dog":
print("わん!")
case "cattle":
print("モー")
case _:
raise RuntimeError
しかし、このままでは文字列リテラルをそのまま使用しているため、理想的な実装とはいえない。定数を定義する。
# 例2
CAT = "cat"
DOG = "dog"
CATTLE = "cattle"
def take_match_action(animal):
match animal:
case CAT:
print("にゃー")
case DOG:
print("わん!")
case CATTLE:
print("モー")
# SyntaxError: name capture 'CAT' makes remaining patterns unreachable
# 例3
import enum
# 列挙型
class AnimalEnum(enum.Enum):
CAT = "cat"
DOG = "dog"
CATTLE = "cattle"
def take_match_action(animal):
match animal:
case AnimalEnum.CAT:
print("にゃー")
case AnimalEnum.DOG:
print("わん!")
case AnimalEnum.CATTLE:
print("モー")
さて、私は今2通りの書き方をした。これが今回のキャプチャーパターン、値パターンに関係のあるコードだ。
例2はキャプチャーパターンであり、実行するとSyntaxErrorを吐く。例3は値パターンであり、正常に動作する。
例2では値パターンではなくキャプチャーパターンとしてPythonが処理しているため、一番最初のcase文が常に真になってしまうがためにエラーが出てしまう。
では一見正しく動きそうなプログラムが、なぜ値パターンではなくキャプチャーパターンとして処理され、エラーを吐いてしまうのか。
ここでPythonの公式の値パターンのEBNFを見たいと思う。
EBNFについて簡単に説明するとA ::= Bという文はAをBと定義するという意味である。また|はorを表している。
value_pattern ::= attr
attr ::= name_or_attr "." NAME
name_or_attr ::= attr | NAME
これを見るとNAME.NAMEまたはattr.NAME(ドットで区切られた識別子の連鎖: NAME.NAME.NAMEなど)で構成されるものを値パターンとするということになる。つまり、NAME単体では値パターンとしてPythonが判断してくれないということである。
次にキャプチャーパターンのEBNFを見ると、
capture_pattern ::= !'_' NAME
!は否定なのでアンダースコア以外のNAMEをキャプチャーパターンとするということである。
ちなみに、アンダースコアはワイルドカードパターンであり、常に真になる。つまり、c言語で言えばdefaultのような使い方をする。
※ 正確には異なるものの、簡単化のためにNAMEは変数名や関数名を表しているものとして解釈して欲しい
ここまで理解できたら、なぜ例3のように書かなければならないかわかったと思う。
NAME.NAMEやNAME.NAME.NAMEのように書かなければPythonが値パターンとしてみてくれないからだ。ナンテコッタ。
終わりに
このキャプチャーパターンと値パターンの違いはなかなか難しいのにクリティカルなので、この記事が救いになったら嬉しい。もしよくわからなくなったら、誤解を恐れずにいうととりあえずパターンに「.」が含まれていれば値パターンになる、ということだけ覚えていればいい。