今年も、Pythonのメジャーリリースの季節がやってまいりました。
Python 3.9から、Pythonのメジャーバージョンアップは年に一度、10月に行われるようになりました。Python 3.10は一年周期のリリースに切り替わってから、2度めのリリースです。
Python 3.9の次のバージョン番号は4.0? と思っていた方も多かったようですが、4.0ではなく3.10となります。
このシリーズでは、何回かにわけてPython 3.10の新機能を紹介していきます。まず、Pythonの新たな文法として追加された構造的パターンマッチ について、簡単に紹介します。
構造的パターンマッチ¶
パターンマッチは、将来のPythonのコーディングスタイルに大きな影響を与えると思われる、重要な機能です。けっこう複雑な機能ですが、しっかり理解しておきましょう。
構造的パターンマッチの詳細は、次のPEPで解説されています。
- PEP 634, Structural Pattern Matching: Specification
- PEP 635, Structural Pattern Matching: Motivation and Rationale
- PEP 636, Structural Pattern Matching: Tutorial
定数値によるパターンマッチ¶
C言語やJavaなどには、値に従って処理を分岐する switch 文があります。
例えば、Pythonの次のような処理は、
if var1 == 100:
print("百")
elif var1 == 1000:
print("千")
else:
print("その他")
C言語では switch 文を使ってこんな感じにかけます。
switch (var1) {
case 100:
printf("百\n"):
break;
case 1000:
printf("千\n"):
break;
default:
printf("その他\n");
}
これだけ見ると、switch を使うメリットはそれほどわかりませんね。
実際、このような switch 文は、過去にも何度かPythonへの導入が検討されましたが、あまり必要とはされていませんでした。Pythonの発明者であるGuido van Rossum も2006年に PEP 3103 -- A Switch/Case Statement で提案しましたが、採用には至りませんでした。
しかし、時は移って2021年、ついにPython 3.10ではパターンマッチを使って次のように書けるようになります。
match var1:
case 100:
print("百")
case 1000:
print("千")
case _:
print("その他")
パターンマッチでは、match 値: で指定した値にマッチする条件を持つ case ブロックをみつけ、その処理を実行します。ここでは、case に 100 や 1000 などの整数値を指定し、var1 の値に従って処理を分岐しています。
このように case で指定する条件を、パターン と呼びます。
match 式:
case パターン:
処理
case パターン:
処理
case _:
処理
マッチする case が見つからなければ、パターンが _ の処理が実行されます。_ は他にマッチするパターンがなかったときに選択されるパターンで、ワイルドカード といいます。
パターンの文法¶
さて、先程の例で if 文を使って
if var1 == 100:
print('百')
と書きましたが、このプログラムは少し修正して
if var1 == (99 + 1):
print('百')
としても、結果は全く同じです。後者のように書いても、比較演算の前に 99 + 1 の計算が行われ、100 という整数値と比較されるからです。
しかし、パターンマッチの場合は違ってきます。
match var1:
case 100:
print("百")
の 100 を 99+1 に書き換えて、
match var1:
case 99+1:
print("百")
を実行した場合、次のようなエラーとなります。
>>> match var1:
... case 99+1:
File "<stdin>", line 2
case 99+1:
^
SyntaxError: imaginary number required in complex literal
99+1 という式はPythonの式としてなんの問題もありませんが、caseのパターンには使えません。パターンの文法は、通常のPythonの式とは全く異なっているのです。
リスト・タプルのパターンマッチ¶
C言語の switch では、case には単純な定数の値しか指定できません。しかし、パターンマッチでは、もっと複雑にデータの種類や属性などを指定できます。
別の例として、伝染病のワクチンの接種履歴データを処理するプログラムを考えてみましょう。
このプログラムで処理するデータは、ワクチンの摂取回数によって異なリます。一回しか接種していない人のデータは 氏名 と 一回目の接種日 からなる、長さ2のタプルです。
(氏名, 一回目の接種日)
二回摂取している人のデータは、二回めの接種日 を追加した、長さ3のタプルとなります。
(氏名, 一回目の接種日, 二回目の接種日)
このプログラムは、パターンマッチを使わない、伝統的な方法だと次のようになるでしょう。
for rec in records:
if len(rec) == 2: # 一回接種
name, date1 = rec
vaccinated_once(name, date1)
elif len(rec) == 3: # 二回接種
name, date1, date2 = rec
vaccinated_twice(name, date1, date2)
else:
raise ValueError("Invalid record")
パターンマッチを使うと、このように書けます。
for rec in records:
match rec:
case [name, date1]: # 一回接種
vaccinated_once(name, date1)
case [name, date1, date2]: # 二回接種
vaccinated_twice(name, date1, date2)
case _: # ワイルドカード
raise ValueError("Invalid record")
パターンにシーケンスを指定する¶
ここでは、パターンとして
[name, date1]
と指定しています。[] は指定した要素を持つシーケンス(リストやタプルなど) にマッチするという指定です。
[] の中に変数名を指定すると、変数名の数に一致する長さのシーケンスとマッチします。この場合、中に name と date1 の2つの変数名を指定していますので、
['Python Taro', '2021/11/11']
のような、要素が2つのリストなどにマッチします。
シーケンスから値を取得する¶
マッチした場合、先頭の要素を変数 name に、二番めの要素を変数 date1 に代入します。パターンマッチでは、分岐条件を指定するだけでなく、対象の値からデータを取り出し、変数に代入する処理も一緒に指定できるのも特徴です。
次の
[name, date1, date2]
というパターンも同様で、要素が3つのシーケンスにマッチし、マッチした場合は値をそれぞれ name, date1, date2 に代入します。
パターンマッチを使うと、if 文をつかった従来の記述にくらべ、処理の条件が明確で、見通しが良くなったのではないでしょうか?また、シーケンスの要素と変数の対応もわかりやすく、わかりやすいプログラムになっています。
辞書のマッチ¶
パターンマッチでは、辞書オブジェクトもマッチングできます。
例題として、GithubのAPIを使ってPythonプロジェクトのプルリクエスト を取得し、ステータスと番号、タイトルを出力してみます。Githubとの通信には、requests パッケージを使っています。
パターンマッチを使わない書き方では、こんな感じでしょうか。Githubから取得した辞書オブジェクトから、キーがstate と number、titleの要素を取得して出力します。state が closed の場合は、クローズした日時も closed_at キーから取得して出力します。
import requests
pulls = requests.get("https://api.github.com/repos/python/cpython/pulls?state=all").json()
for pull in pulls:
if pull['state'] == 'open':
print("オープン:", pull['number'], pull['title'])
elif pull['state'] == 'closed':
print("クローズ:", pull['number'], pull['title'], pull['closed_at'])
パターンマッチを使うと、次のように書けます。
import requests
pulls = requests.get("https://api.github.com/repos/python/cpython/pulls?state=all").json()
for pull in pulls:
match pull:
case {'state': 'open', 'number': number, 'title': title}:
print("オープン:", number, title)
case {'state': 'closed', 'number': number,
'title': title, 'closed_at': closed_at}:
print("クローズ:", number, title, closed_at)
パターンに辞書を指定する¶
こんどは、パターンとして
{'state': 'open', 'number': number, 'title': title}
と指定しています。{}は、辞書オブジェクトにマッチするという指定です。
'state': 'open' と指定した場合、辞書の 'state' というキーの値が文字列 'open' であればマッチします。
また、 'number': number や 'title': title のように、キーの値と変数名を指定すると、そのキーが存在する辞書にマッチします。キーに対応する値は、変数に代入されます。
まとめると、このパターンでは、
- キー
'state'の値が文字列'open'に等しく 'number'、'title'の2つのキーが登録されている
辞書にマッチします。'number'、'title' に対応する値は、それぞれ変数 number、title に代入されます
パターンマッチを使うと、処理の分岐と値の取り出しがひとまとめで済むので、見通しがよいですね。
2つ目のパターン
{'state': 'closed', 'number': number,
'title': title, 'closed_at': closed_at}
も同様です。
こちらのパターンは、
'state'の値が文字列'closed'に等しく'number'、'title'、'closed_at'の3つのキーが登録されている
辞書にマッチします。'number'、'title'、'closed_at' に対応する値は、それぞれ変数 number、title、closed_at に代入されます
その他にも¶
このページでは、Pythonの新機能、構造的パターンマッチを紹介しました。パターンマッチはけっこう複雑で、ここで紹介したのはごく一部に過ぎません。また、まだまだ新しい機能のため、仕様が十分に詰められていない部分も残っています。
しかし、非常に便利な機能がおおく、これからのPythonプログラミングの重要な位置を占めると思われます。ぜひともマスターしてみてください。