Introduction to Structural Pattern Matching

Takanori Suzuki

PyCon Kyushu 2022 Kumamoto / 2022 Jan 22

Agenda / アジェンダ

  • Motivation / モチベーション

  • What’s New / 更新情報

  • Syntax / 構文

  • Patterns / パターン

Photos 📷 Tweets 🐦 👍

#pycon9ku / @takanory

Slide 💻

slides.takanory.net

Who am I? / お前誰よ 👤

../_images/sokidan-square.jpg

この発表の モチベーション 💪

  • Structural Pattern Matching は 便利そう

  • みんなに 知って使って みてほしい

この発表の ゴール 🥅

  • 構文基本的な使い方 を知る

  • さまざまな パターン と、その 使い方 を知る

  • 明日から 試せる

前提条件

  • 中級 レベル

  • Pythonの文法 を理解している

    • タプル、リスト、辞書、if、def、isinstance、データクラス、型ヒントなど

質問

Python 3.10を使ってますか? 🙋‍♂️

3.10の新機能を知ってますか? 🙋‍♀️

What’s New in Python 3.10 🆕

What’s New in Python 3.10 🆕

What's New in Python 3.10

Python Release Python 3.10.0

www.python.org/downloads/release/python-3100/

Python Release Python 3.10.0

お前誰よ? 🐍

Python 3.10 release logo

Python 3.10の 新機能

  • Parenthesized Context Managers

  • Better Typing Syntax

  • Better Error Messages

  • Structural Pattern Matching

  • Better Debugging

Python 3.10の 新機能

  • Parenthesized Context Managers

  • Better Typing Syntax

  • Better Error Messages

  • Structural Pattern Matching 👈

  • Better Debugging

Structural Pattern Matching 🏛

Structural Pattern Matching 🏛

モチベーション

www.python.org/dev/peps/pep-0635/#motivation

(Structural) pattern matching syntax is found in many languages, from Haskell, Erlang and Scala to Elixir and Ruby. (A proposal for JavaScript is also under consideration.)

モチベーション

www.python.org/dev/peps/pep-0635/#motivation

(構造的)パターンマッチの構文は、Haskell、Erlang、ScalaからElixir、Rubyなど、多くの言語で見られます(JavaScriptへの提案も検討中)。

モチベーション

# オブジェクトの型や形を確認する
if isinstance(x, tuple) and len(x) == 2:
    host, port = x
    mode = "http"
elif isinstance(x, tuple) and len(x) == 3:
    host, port, mode = x
# Structural Pattern Matching
match x:
    case host, port:
        mode = "http"
    case host, port, mode:
        pass

構文

  • Pattern Matchingの基本的な構文

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

ソフト キーワード

  • Python 3.10の 新仕様

  • matchcase_

  • 識別子 に使用可能

>>> match = 'match'  # OK
>>> class = 'class'  # NG
  File "<stdin>", line 1
    class = 'class'  # NG
          ^
SyntaxError: invalid syntax

パターン

パターン

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

リテラル パターン

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

OR パターン

  • | は OR

match beer_style:
    case "Pilsner":
        result = "First drink"
    case "IPA" | "Session IPA":
        result = "I like it"
    case "Hazy IPA":
        result = "Cloudy and cloudy"
    case _:
        result = "I like most beers"

wildcardなし のLiteralパターン

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

? 🤔

if 文で書き換える

  • if 文で書いた場合

if beer_style == "Pilsner":
    result = "First drink"
elif beer_style == "IPA" or beer_style == "Session IPA":
    result =  "I like it"
elif beer_style == "Hazy IPA":
    result = "Cloudy and cloudy"
else:
    result = "I like most beers"

Pattern Matchingは パワフル 💪

リテラルと 変数 パターン

リテラルと 変数 パターン

  • 長さ2のタプルが注文を表す

order1 = ("IPA", "nuts")  # ビールとフード
order2 = ("Pilsner", "")  # ビールのみ
order3 = ("", "fries")    # フードのみ
order4 = ("", "")         # なにも注文しない

order_beer_and_food(order1)  # -> I dring IPA with nuts.

リテラルと 変数 パターン

def order_beer_and_food(order: tuple) -> str:
    match order:
        case ("", ""):
            return "Please order something."
        case (beer, ""):
            return f"I drink {beer}."
        case ("", food):
            return f"I eat {food}."
        case (beer, food):
            return f"I drink {beer} with {food}."
        case _:
            return "one beer and one food only."

リテラルと 変数 パターン

def order_beer_and_food(order: tuple) -> str:
    match order:
        case ("", ""):  # match here
            return "Please order something."
        case (beer, ""):
            return f"I drink {beer}."
        case ("", food):
            return f"I eat {food}."
        case (beer, food):
            return f"I drink {beer} with {food}."
        case _:
            return "one beer and one food only."

order_beer_and_food(("", ""))  # -> Please order something.

リテラルと 変数 パターン

  • "IPA"beer に代入

def order_beer_and_food(order: tuple) -> str:
    match order:
        case ("", ""):
            return "Please order something."
        case (beer, ""):  # match here
            return f"I drink {beer}."
        case ("", food):
            return f"I eat {food}."
        case (beer, food):
            return f"I drink {beer} with {food}."
        case _:
            return "one beer and one food only."

order_beer_and_food(("IPA", ""))  # -> I drink IPA.

リテラルと 変数 パターン

  • "IPA"beer に代入

  • "nuts"food に代入

def order_beer_and_food(order: tuple) -> str:
    match order:
        case ("", ""):
            return "Please order something."
        case (beer, ""):
            return f"I drink {beer}."
        case ("", food):
            return f"I eat {food}."
        case (beer, food):  # match here
            return f"I drink {beer} with {food}."
        case _:
            return "one beer and one food only."

order_beer_and_food(("IPA", "nuts"))  # -> I drink IPA with nuts.

リテラルと 変数 パターン

  • タプルの長さが一致しない

def order_beer_and_food(order: tuple) -> str:
    match order:
        case ("", ""):
            return "Please order something."
        case (beer, ""):
            return f"I drink {beer}."
        case ("", food):
            return f"I eat {food}."
        case (beer, food):
            return f"I drink {beer} with {food}."
        case _:  # match here
            return "one beer and one food only."

order_beer_and_food(("IPA", "nuts", "spam"))  # -> one beer and one food only.

if 文で書き換える

def order_beer_and_food(order: tuple) -> str:
    if len(order) == 2:
        beer, food = order
        if beer == "" and food == "":
            return  "I'm full."
        elif beer != "" and food == "":
            return f"I drink {beer}."
        elif beer == "" and food != "":
            return f"I eat {food}."
        else:
            return f"I drink {beer} with {food}."
    else:
        return  "one beer and one food only."

どっちが好み?

  • Structural Pattern Matching

  • if

順番 は重要 ⬇️

def order_beer_and_food(order: tuple) -> str:
    match order:
        case (beer, food):  # match here
            return f"I drink {beer} with {food}."
        case ("", ""):  # never reach
            return "Please order something."
        case (beer, ""):  # never reach
            return f"I drink {beer}."
        case ("", food):  # never reach
            return f"I eat {food}."
        case _:
            return "one beer and one food only."

order_beer_and_food(("IPA", ""))  # -> I drink IPA with .

クラス パターン

クラス パターン

@dataclass
class Order:  # Order(beer="IPA"), Order("Ale", "nuts")...
    beer: str = ""
    food: str = ""
def order_with_class(order: Order) -> str:
    match order:
        case Order(beer="", food=""):
            return "Please order something."
        case Order(beer=beer, food=""):
            return f"I drink {beer}."
        case Order(beer="", food=food):
            return f"I eat {food}."
        case Order(beer=beer, food=food):
            return f"I drink {beer} with {food}."
        case _:
            return "Not an order."

クラスパターンの 実行結果

>>> order_with_class(Order())
'Please order something.'
>>> order_with_class(Order(beer="Ale"))
'I drink Ale.'
>>> order_with_class(Order(food="fries"))
'I eat fries.'
>>> order_with_class(Order("Ale", "fries"))
'I drink Ale with fries.'
>>> order_with_class("IPA")
'Not an order.'

クラスパターン

def order_with_class(order: Order) -> str:
    match order:
        case Order(beer="", food=""):
            return "Please order something."
        case Order(beer=beer, food=""):
            return f"I drink {beer}."
        case Order(beer="", food=food):
            return f"I eat {food}."
        case Order(beer=beer, food=food):
            return f"I drink {beer} with {food}."
        case _:
            return "Not an order."

if 文で書き換える

def order_with_class(order: Order) -> str:
    if isinstance(order, Order):
        if order.beer == "" and order.food == "":
            return  "Please order something."
        elif order.beer != "" and order.food == "":
            return f"I drink {order.beer}."
        elif order.beer == "" and order.food != "":
            return f"I eat {order.food}."
        else:
            return f"I drink {order.beer} with {order.food}."
    else:
        return "Not an order."

注文用 クラス

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

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

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

複数のクラス を使うパターン

def order_with_classes(order: Beer|Food|Water) -> str:
    match order:
        case Beer(style=style, size=size):
            return f"I drink {size} of {style}."
        case Food(name=name):
            return f"I eat {name}."
        case Water(number=number):
            return f"{number} glasses of water, please."
        case _:
            return "Not an order."

if 文で書き換える

def order_with_classes(order: Beer|Food|Water) -> str:
    if isinstance(order, Beer):
        return f"I drink {order.size} of {order.style}."
    elif isinstance(order, Food):
        return f"I eat {order.name}."
    elif isinstance(order, Water):
        return f"{order.number} glasses of water, please."
    else:
        return "Not an order."

宣伝 📣

../_images/python-recipes-book.jpg

Python実践レシピ 📕

  • 2022年1月19日発売

  • 鈴木たかのり、筒井隆次、寺田学、杉田雅子、門脇諭、福田隼也著

  • B5変形判 / 512ページ / 2,970円

  • クロージングで プレゼント あるかも

宣伝終わり

シーケンス パターン ➡️

注文テキストを解析

  • リストに変換してパターンマッチ

order_text = "beer IPA pint"
order_text.split()  # -> ["beer", "IPA", "pint"]

order_text = "food nuts"
order_text = "water 3"
order_text = "bill"

シーケンスの 長さ でマッチ

match order_text.split():
    case [action]:
        # ["bill"] にマッチ
        ...
    case [action, name]:
        # ["food", "nuts"]、["water", "3"] にマッチ
        ...
        # 処理を分岐したい
    case [action, name, size]:
        # ["beer", "IPA", "pint"] にマッチ
        ...

特定の値 にマッチ

  • 特定の値(bill, food…)にマッチ

  • シーケンス + リテラル パターン

match order_text.split():
    case ["bill"]:  # ["bill"] にのみマッチ
        calculate_amount()
    case ["food", food]:  # ["food", "nuts"]
        tell_kitchen(food)
    case ["water", number]:  # ["water", "3"]
        glass_of_water(number)
    case ["beer", style, size]:  # ["beer", "IPA", "pint"]
        tell_beer_master(style, size)

任意の値 にマッチ

  • 有効なビールサイズ: pinthalf

  • "beer IPA 1-liter" はマッチしない

match order_text.split():
    ...
    case ["beer", style, ("pint" | "half")]:  # ORパターン
        # tell_beer_master(style, size)
        # ビールのサイズはどっち?

AS パターン

  • サブパターン の値を取得

  • サイズ(pint または half)を size に代入

match order_text.split():
    ...
    case ["beer", style, ("pint" | "half") as size]:
        tell_beer_master(style, size)

任意の長さの値 にマッチ

  • 複数の料理の注文に対応する

  • 例: "food nuts fries pickles"

order_text = "food nuts fries pickles"

match order_text.split():
    ...
    case ["food", food]:  # マッチしない
        tell_kitchen(food)

任意の長さの値 にマッチ

  • 変数名に アスタリスク (*)を追加

order_text = "food nuts fries pickles"

match order_text.split():
    ...
    case ["food", *foods]:  # 任意の長さの値をキャプチャ
        for food in foods:  # ("nuts", "fries", "pickles")
            tell_kitchen(name)

マッピング パターン 📕

マッピング パターン 📕

  • 辞書 用のパターン

  • JSON の解析に便利

order_dict = {"beer": "IPA", "size": "pint"}

match order_dict:
    case {"food": food}:
        tell_kitchen(food)
    case {"beer": style, "size": ("pint" | "half") as size}:
        tell_beer_master(style, size)
    case {"beer": style, "size": _}:
        print("Unknown beer size")
    case {"water": number}:
        glass_of_water(number)
    case {"bill": _}:
        calculate_amount()

組み込み クラスにマッチ

  • 料理名は文字列、水の数は整数

  • str()int() などを使う

order_dict = {"water": 3}
# order_dict = {"water": "three"}  # マッチしない

match order_dict:
    case {"food": str(food)}:
        tell_kitchen(food)
    ...
    case {"water": int(number)}:
        glass_of_water(number)
    ...

ガード 💂‍♀️

ガード 💂‍♀️

  • パターンの後ろに if

  • 水は1〜9杯しか頼めない

order_dict = {"water": 3}  # 有効な値
# order_dict = {"water": 15}  # -> 水は1〜9杯です
# order_dict = {"water": "three"}  # -> 水は数値で指定してください

match order_dict:
    case {"water": int(number)} if 0 < number < 10:
        glass_of_water(number)
    case {"water": int(number)}:
        print("水は1〜9杯です")
    case {"water": _}:
        print("水は数値で指定してください")

まとめ

まとめ

  • モチベーション 💪

  • 構文

    • ソフトキーワード: matchcase_

  • パターン

    • リテラル、ワイルドカード、変数、クラス、シーケンス、マッピング、OR、AS、ガード

Structural Pattern Matching に 挑戦 👍

参考資料 📚

Thank you !! 🙏

Takanori Suzuki ( @takanory)

slides.takanory.net

../_images/sokidan-square.jpg