Python Monthly Topics

Python 3.10の新機能:構造化パターンマッチング

鈴木たかのりです。今月からgihyo.jp上で「Python Monthly Topics」と題して、毎月Python関連の話題になったトピックやツール、ライブラリなどを紹介していきたいと思います。

第1回目はPython 3.10の新機能「構造化パターンマッチング(Structural Pattern Matching⁠⁠」について紹介します。

Python 3.10の新機能

Python 3.10は2021年10月4日にリリースされました。現在のPythonの最新バージョンは3.10.5で、Download Pythonのページからダウンロードできます。

Python 3.10.5のリリースページを見ると、謎のヘビの画像があります。

Python 3.10 release logo
Python 3.10 release logo

この画像はPython 3.10 release logoという画像で、ヘビの周りにPython 3.10の主な新機能が書いてあります。それは以下の5つです。

  • Parenthesized Context Managers
  • Better Typing Syntax
  • Better Error Messages
  • Structural Pattern Matching
  • Better Debugging

今回はそのうち「構造化パターンマッチング(Structural Pattern Matching⁠⁠」を紹介します。

構造化パターンマッチングとは

構造化パターンマッチングはPythonの新しい文法です。match文とcase文を使用して以下のようなコードが書けるようになります。beer_style変数の値によって処理が分岐し、result変数に任意の文字列が入ります。"Pilsner""IPA" などがパターンです。

どのパターンにもマッチしない場合は_にマッチします。この_をワイルドカードパターンと呼びます。

match beer_style:  # Pilsner, IPA, Hazy IPA and others
    case "Pilsner":
        result = "First drink"
    case "IPA":
        result = "I like it"
    case "Hazy IPA":
        result = "Cloudy and cloudy"
    case _:  # Wildcard
        result = "I like most beers"

構造化パターンマッチングのPEP

Pythonで新機能を追加するにはPEP(Python Enhancement Proposal)というドキュメントを書いて、開発者間で議論して採択される必要があります。

構造化パターンマッチングはとても大きな機能のため、3つのPEPが存在します(通常は1つの機能に対して1つのPEPドキュメントです⁠⁠。

この記事では一部機能しか紹介できないので、詳細な仕様やチュートリアルを確認したい方は、ぜひPEPドキュメントを読んでみてください。

構造化パターンマッチングの文法

文法は以下のようになります。subjectの内容がcaseの後ろに書いてあるいずれかのパターンにマッチすると、そのパターンのブロックにあるアクションが実行されます。

なお、パターンはどれか1つにマッチしたら、アクションの実行後にmatchのブロックを抜けます。全てのパターンにマッチせず最後にワイルドカード_が書いてある場合は、ワイルドカードのアクションを実行します。ワイルドカードがない場合はなにもしません。

match subject:
    case <pattern_1>:
        <action_1>
    case <pattern_2>:
        <action_2>
    case <pattern_3>:
        <action_3>
    case _:
        <action_wildcard>

パターンマッチングで使用するmatchcase_ソフトキーワードというPython文法上の新しい概念です。ifforなどのキーワードと異なり、変数名等に使用可能です。

>>> if = "if"
  File "<stdin>", line 1
    if = "if"
       ^
SyntaxError: invalid syntax
>>> match = "match"

※参考:2.3.1. キーワード(Python公式ドキュメント)

構造化パターンマッチングのなにが便利なのか

冒頭のコード例を見て「この機能のなにが便利なのか?if文でよいのでは?」と感じた方も多いと思います。しかし、構造化パターンマッチングではパターンにさまざまな種類があり、if文では複雑になる条件分岐をすっきりと書けるようになります。

キャプチャーパターン

キャプチャーパターンではパターンにマッチしたときに変数に値が代入されます。コード例を見てみましょう。

order = ("beer", "IPA")  # ビールの注文
# order = ("food", "Pizza")  # フードの注文

match order:
    case ("beer", beer):
        print(f"ビール「{beer}」をお持ちしました")
    case ("food", food):
        print(f"フード「{food}」を召し上がれ")

このコードを実行するとビール「IPA」をお持ちしましたというメッセージが出力されます。タプルの1つ目の要素が文字列の"beer"にマッチし、2つ目の要素がbeer変数に代入されています。

このコードをif文で書き換えると以下のようになります。まずorderがタプルやリストなどのSequence型か、そして要素数が2つかを確認します。その後1つ目の要素を確認し、2つ目の要素を取り出しています。

どうでしょうか? 構造化パターンマッチングの方が読みやすい気がしませんか?

from collections.abc import Sequence

if isinstance(order, Sequence) and len(order) == 2:
    if order[0] == "beer":
        beer = order[1]
        print(f"ビール「{beer}」をお持ちしました")
    elif order[0] == "food":
        food = order[1]
        print(f"フード「{food}」を召し上がれ")

他にもさまざまなパターンがあります。

クラスパターン

オブジェクトの型でパターンマッチができます。データクラスと組み合わせて使用すると、コードが読みやすくなります。

※参考:dataclasses --- データクラス(Python公式ドキュメント)

以下のような注文用の3種類のデータクラスを用意します。

from dataclasses import dataclass

@dataclass
class Beer:  # Beer("IPA", "Pint")
    style: str
    size: str

@dataclass
class Food:  # Food("nuts")
    name: str

@dataclass
class Water:  # Water(4)
    number: int

そして、クラスパターンでクラスの種類ごとに分岐し、その値をキャプチャーパターンで取り出しています。

order = Beer("IPA", "Pint")

match (order):
    case Beer(style=style, size=size):
        print(f"{style}{size}サイズでください")
    case Food(name=name):
        print(f"{name}を食べたいです")
    case Water(number=number):
        print(f"水を{number}人分ください")
    case _:
        print("これは注文ではありません")

if文で書き換えると以下のようになります。構造化パターンマッチングの方がシンプルに書けて、意図を読み取りやすいと思います。

if isinstance(order, Beer):
    style = order.style
    size = order.size
    print(f"{style}{size}サイズでください")
elif isinstance(order, Food):
    (略)
else:
    print("これは注文ではありません")

その他のパターン

他のパターンを簡単に紹介します。

  • ORパターン: |で複数のパターンのいずれかにマッチ
  • ASパターン: サブパターンでマッチした値をasで変数に代入
  • マッピングパターン: 辞書などにマッチ
order = "IPA"
# order = ("IPA", "Half")
# order = ("Pilsner", "MASS")
# order = {"beer": "Hazy IPA", "size": "Pint"}

match order:
    case "IPA" | "Hazy IPA":  # ORパターン
        print("好きなスタイル")
    case (style, ("Pint" | "Half") as size):  # ASパターン、("Pint" | "Half")がサブパターン
        print(f"{style}{size}サイズでください")
    case (style, size):
        print(f"{size}サイズはありません")
    case {"beer": style, "size": size}:  # マッピングパターン
        print(f"{style}{size}サイズでください")

まとめ

構造化パターンマッチングについて簡単に紹介しました。便利そうだなと思ってもらえたでしょうか?

単純なif文では不要ですが、複雑な条件分岐になりそうなときは、ぜひ構造化パターンマッチングを使ってみてください(Python 3.10以降の必要があります⁠⁠。コードの見通しがよくなると思います!

参考

おすすめ記事

記事・ニュース一覧