今年も、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プログラミングの重要な位置を占めると思われます。ぜひともマスターしてみてください。