Introduction to Structural Pattern Matching

Takanori Suzuki

PyCon APAC 2023 / 2023 Oct 27

構造的パターンマッチング 入門

Takanori Suzuki

PyCon APAC 2023 / 2023 Oct 27

Agenda / アジェンダ

  • Motivation and Goal / モチベーションとゴール

  • What’s New

  • Syntax / 構文

  • Patterns / パターン

Photos 📷 Tweets 🐦 👍

#pyconapac / #pyconapac_4

@takanory

Slide / スライド 💻

slides.takanory.net

slides.takanory.net

Who am I? / お前 誰よ 👤

takanory profile kuro-chan and kuri-chan

PyCon JP Association 🐍

pycon jp logo

PyCon JP Association Meeting 2023 🍱

  • Oct 27, 12:30-13:30, 20F open space

  • Panelists: PyCon JP Association Board Members

  • Topics

    • Activities and financial report

    • Various discussions, so please join!

BeProud inc. 🏢

  • BeProud: Pythonシステム開発、Consulting

  • connpass: IT勉強会支援プラットフォーム

  • PyQ: Python独学プラットフォーム

  • TRACERY: システム開発ドキュメントサービス

BeProd logos

BeProud booth

We are hiring!!

BeProud Booth

Motivation of this talk 💪

発表のモチベーション

  • Structural Pattern Matching looks useful

  • You to know and use it

Goal of this talk 🥅

発表のゴール

  • Learn syntax and basic usage

  • Learn various patterns and how to use them

  • Try it tomorrow

Prerequisites

前提条件

  • Intermediate level

  • You should know Python syntax

    • tuple, list, dict, if, def, isinstance, dataclass, type hinting and more

Questions

質問

Are you using Python 3.10+? 🙋‍♂️

Python 3.10以上 を使っている人?

Do you know the new features? 🙋‍♀️

新機能 を知っている人?

What’s New in Python 3.10 🆕

Python 3.10の 新機能

What’s New in Python 3.10 🆕

What's New in Python 3.10

Python Release Python 3.10.11 🐍

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

Python Release Python 3.10.11

Who are You? / お前は 誰よ? 🐍

Python 3.10 release logo

New features of Python 3.10

  • Parenthesized Context Managers

  • Better Typing Syntax

  • Better Error Messages

  • Structural Pattern Matching

  • Better Debugging

New features of Python 3.10

  • Parenthesized Context Managers

  • Better Typing Syntax

  • Better Error Messages

  • Structural Pattern Matching 👈

  • Better Debugging

Structural Pattern Matching

構造的パターンマッチング

Structural Pattern Matching

Motivation / モチベーション

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.)

Motivation / モチベーション

# check type or shape of an object
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

Syntax / 構文

  • Generic syntax of pattern matching

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

Soft keywords / ソフト キーワード

  • New in Python 3.10

  • match, case and _

  • Can be used identifier names

>>> match = "match"  # Valid(Soft keyword)
>>> class = "class"  # Invalid(Keyword)
  File "<stdin>", line 1
    class = "class"
          ^
SyntaxError: invalid syntax

Patterns / パターン

Patterns / パターン

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

Literal patterns

リテラル パターン

match beer_style:  # "Pilsner" / "IPA" / "Ale"
   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 patterns

OR パターン

  • | is OR

match beer_style:  # "IPA" / "Session IPA"
    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"

Literal patterns without wildcard

ワイルドカードなし のリテラルパターン

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

Useful? 🤔

便利そう?そうでもない?

rewrite with if statement

if 文で書き直す

  • If written as an if statement

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"

Looks similer 👯

よく 似ている

Pattern Matching is Powerful 💪

パターンマッチングは 強力

Literal and Variable patterns

リテラルと 変数 パターン

Literal and Variable patterns

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."

Literal and Variable patterns

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."

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

Literal and Variable patterns

  • "IPA" assign to beer

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."

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

Literal and Variable patterns

  • "IPA" assign to beer"nuts" assign to 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):
            return f"I drink {beer} with {food}."
        case _:
            return "one beer and one food only."

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

Literal and Variable patterns

  • Tuple length does not match

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."

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

rewrite with if statement

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."

Which code do you prefer?

どっちのコードが 好み

  • Pattern Matching 🆚 if statement

Case Order is important ⬇️

ケースの順番 は重要

Case Order is important ⬇️

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 = ("", "nuts")
print(order_beer_and_food(order))  # -> I drink  with nuts.

Classes patterns

クラス パターン

Classes patterns

@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."

Results: Classes patterns

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

>>> 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.'

Classes patterns

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."

rewrite with if statement

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."

Use multiple classses

複数 のクラスを使用

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

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

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

Use multiple classses

複数 のクラスを使用

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."

rewrite with if statement

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."

Sequense patterns ➡️

シーケンス パターン

Sequense patterns ➡️

  • Parse the order text

  • for example:

order_text = "beer IPA pint"
order_text = "food nuts"
order_text = "water 3"
order_text = "bill"

Matching by length of sequence

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

match order_text.split():
    case [action]:  # match "bill"
        ...
    case [action, name]:  # match "food nuts", "water 3"
        ...
    case [action, name, size]:  # match "beer IPA pint"
        ...

Matching specific values in sequence

シーケンス中の 任意の値 でマッチ

  • Specific values: "bill", "food"

match order_text.split():
    case ["bill"]:  # match "bill"
        calculate_amount()
    case ["food", food]:  # match "food nuts"
        tell_kitchen(food)
    case ["water", number]:  # match "water 3"
        grass_of_water(number)
    case ["beer", style, size]:  # match "beer IPA pint"
        tell_beer_master(style, size)

Capturing matched sub-patterns

マッチした サブパターン を捕捉する

  • Valid beer size: "Pint" or "HalfPint"

  • "beer IPA Small" is invalid

order_text = "beer IPA Pint"

match order_text.split():
    ...
    case ["beer", style, ("Pint" | "HalfPint")]:
        # I don't know beer size

Capturing matched sub-patterns

マッチした サブパターン を捕捉する

  • Use as patterns

  • ("Pint" | "HalfPint") as size

order_text = "beer IPA Pint"

match order_text.split():
    ...
    case ["beer", style, ("Pint" | "HalfPint") as size]:
        tell_beer_master(style, size)  # size is "Pint"

Matching multiple values

複数の値 にマッチ

  • I want to order several foods

  • example: "food nuts fries pizza"

order_text = "food nuts fries pizza"

match order_text.split():
    ...
    case ["food", food]:  # capture single value
        tell_kitchen(food)

Matching multiple values

複数の値 にマッチ

  • food*foods

order_text = "food nuts fries pizza"

match order_text.split():
    ...
    case ["food", *foods]:  # capture multiple values
        for food in foods:  # ("nuts", "fries", "pizza")
            tell_kitchen(food)

I can order several foods!! 🍟🍕

複数の食べ物を注文できる!!

Mapping Patterns 📕

マッピング パターン

Mapping Patterns 📕

  • Pattern match for dict

  • Useful for alalyzing JSON

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

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

Matching builtin classes

組み込み クラスにマッチ

  • Use str(), int() and more

order_dict = {"water": 3}
# order_dict = {"water": "three"}  # Doesn't match

match order_dict:
    case {"food": str(food)}:
        tell_kitchen(food)
    case {"beer": str(style), "size": ("Pint" | "HalfPint") as size}:
        tell_beer_master(style, size)
    case {"beer": style, "size": _}:
        print("Unknown beer size")
    case {"water": int(number)}:
        grass_of_water(number)
    case {"bill": _}:
        calculate_amount()

Guards 💂‍♀️

ガード

Number of water

水の

  • Invalid: 9, 10, …

  • Valid: 1, 2, … 8

  • Invalid: 0, -1, …

Guards 💂‍♀

  • Valid: 1, 2, … 8

  • if statement after pattern

order_list = ["water", 3]  # -> 3 glasses of water, please.
# order_list = ["water", 15]  # -> 15 is invalid value.

match order_list:
    case ["water", int(number)] if 1 <= number <= 8:
        print(f"{number} glasses of water, please.")
    case ["water", _]:
        print(f"{number} is invalid value.")

Can’t order many glasses of water! 🎉

たくさんの水 を注文できない!

Summary / まとめ

Summary / まとめ

  • Motivation and Goal / モチベーションとゴール

  • What’s New

  • Syntax / 構文

    • Soft keywords: match, case and _

  • Patterns / パターン

    • Literal, Variable, Classes, Sequense, Mapping

    • Wildcard(_), OR(|), AS, Guards

Try Structural Pattern Matching 👍

構造的パターンマッチングを 試そう

References 📚

Thank you !! 🙏

slides.takanory.net

@takanory takanory takanory takanory

takanory profile kuro-chan and kuri-chan

Finally, 2 important notices

最後に、2つの 重要な お知らせ

1: PyCon APAC 2023 Original Beer

PyCon APAC 2023 オリジナルビール

PyCon APAC 2023 original beer

Only available at Official party

オフィシャルパーティー でのみ飲めます

  • I prepared beer in → Video

  • If you can drink beer, you should join the party!!

Beer preparing

2: Unofficial Sprint after-party

非公式 Sprint 打ち上げ

Unofficial Sprint after-partu

Thank you !! 🙏

slides.takanory.net

@takanory takanory takanory takanory

takanory profile kuro-chan and kuri-chan