Python3.10の新機能 Python 3.10の新機能(その1) パターンマッチ

(その1) パターンマッチ(その2) with文のネスト(その3) エラーメッセージの改善(その4) | 演算子によるユニオン型の指定(その5) パラメータ仕様変数(その6) 明示的な型エイリアス(その7) ユーザ定義型ガード(その8) OpenSSL 1.1.1が必須に(その9) zip()関数に引数 strict を追加(その10) Dataclassでslotsが利用可能に(その11) その他の変更

今年も、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で解説されています。

定数値によるパターンマッチ

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 ブロックをみつけ、その処理を実行します。ここでは、case1001000 などの整数値を指定し、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("百")

10099+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]

と指定しています。[] は指定した要素を持つシーケンス(リストやタプルなど) にマッチするという指定です。

[] の中に変数名を指定すると、変数名の数に一致する長さのシーケンスとマッチします。この場合、中に namedate1 の2つの変数名を指定していますので、

['Python Taro', '2021/11/11']

のような、要素が2つのリストなどにマッチします。

シーケンスから値を取得する

マッチした場合、先頭の要素を変数 name に、二番めの要素を変数 date1 に代入します。パターンマッチでは、分岐条件を指定するだけでなく、対象の値からデータを取り出し、変数に代入する処理も一緒に指定できるのも特徴です。

次の

[name, date1, date2]

というパターンも同様で、要素が3つのシーケンスにマッチし、マッチした場合は値をそれぞれ name, date1, date2 に代入します。

パターンマッチを使うと、if 文をつかった従来の記述にくらべ、処理の条件が明確で、見通しが良くなったのではないでしょうか?また、シーケンスの要素と変数の対応もわかりやすく、わかりやすいプログラムになっています。

辞書のマッチ

パターンマッチでは、辞書オブジェクトもマッチングできます。

例題として、GithubのAPIを使ってPythonプロジェクトのプルリクエスト を取得し、ステータスと番号、タイトルを出力してみます。Githubとの通信には、requests パッケージを使っています。

パターンマッチを使わない書き方では、こんな感じでしょうか。Githubから取得した辞書オブジェクトから、キーがstatenumbertitleの要素を取得して出力します。stateclosed の場合は、クローズした日時も 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 のように、キーの値と変数名を指定すると、そのキーが存在する辞書にマッチします。キーに対応する値は、変数に代入されます。

まとめると、このパターンでは、

  1. キー 'state' の値が文字列 'open' に等しく
  2. 'number''title' の2つのキーが登録されている

辞書にマッチします。'number''title' に対応する値は、それぞれ変数 numbertitle に代入されます

パターンマッチを使うと、処理の分岐と値の取り出しがひとまとめで済むので、見通しがよいですね。

2つ目のパターン

{'state': 'closed', 'number': number,
 'title': title, 'closed_at': closed_at}

も同様です。

こちらのパターンは、

  1. 'state' の値が文字列 'closed' に等しく
  2. 'number''title''closed_at' の3つのキーが登録されている

辞書にマッチします。'number''title''closed_at' に対応する値は、それぞれ変数 numbertitleclosed_at に代入されます

その他にも

このページでは、Pythonの新機能、構造的パターンマッチを紹介しました。パターンマッチはけっこう複雑で、ここで紹介したのはごく一部に過ぎません。また、まだまだ新しい機能のため、仕様が十分に詰められていない部分も残っています。

しかし、非常に便利な機能がおおく、これからのPythonプログラミングの重要な位置を占めると思われます。ぜひともマスターしてみてください。

Copyright © 2001-2021 python.jp Privacy Policy python_japan
Amazon.co.jpアソシエイト
Amazonで他のPython書籍を検索