Python's Structural Pattern Matching

課程節錄

simple matching

  1. 和傳統我們熟悉的 switch 很接近。

  2. wildcard(通配符): 使用 underscore(下劃線) _ 作為「預設」匹配模式。

multiple pattern matching

  1. OR(|) 模式:一次匹配多個文字

    case "Java" | "Javascript":
    
  2. capturing matched sub-patterns 捕獲匹配子模式:

    case ["move", ("F" | "B" | "L" |"R") as direction]:
        return symbols[direction]
    
  3. 上述語法的簡化:不用每次都打 op(["move", "L"]),而是連續 op(["move", "F", "F", "L"])

    case ['move', *directions]:
        return tuple(symbols[direction] for direction in directions)
    
  4. 上述語法在輸入資料不存在時會報錯 op(["move", "up"]),解決:

    case ['move', *directions] if set(directions) < symbols.keys():
        return tuple(symbols[direction] for direction in directions)
    

pep 634 後更新 (官網定義)

因已針對 pep 634 更新,上述連結並非 pep 634,而是更新後的說明。

語法

pep 634 語法 & 更新語法說明:只是寫法稍有不同

match_stmt: "match" subject_expr ':' NEWLINE INDENT case_block+ DEDENT
subject_expr:
    | star_named_expression ',' star_named_expressions?
    | named_expression
case_block: "case" patterns [guard] ':' block
guard: 'if' named_expression
match_stmt   ::=  'match' subject_expr ":" NEWLINE INDENT case_block+ DEDENT
subject_expr ::=  star_named_expression "," star_named_expressions?
                  | named_expression
case_block   ::=  'case' patterns [guard] ":" block

以下針對上述語法分別說明:

match 語法示意圖

match_stmt   ::=  'match' subject_expr ":" NEWLINE INDENT case_block+ DEDENT

subject_expr

subject_expr ::=  star_named_expression "," star_named_expressions?
                  | named_expression

語法說明中的 ?,表示這個部分是可選的(optional),可以有許多個或者沒有。

語法說明中的 |,表示選項中擇一。

這裡的意思是:subject_expr 可以有兩種寫法(|):

  1. star_named_expression “,” star_named_expressions?

    star_named_expression

    star_named_expression, star_named_expression

    star_named_expression, star_named_expression, star_named_expression

  2. named_expression

程式示範:

# 定義一個函式,接受兩種不同結構的參數作為主題表達式
def process_data(subject_expr):
    match subject_expr:
        # 第一種情況:star_named_expression "," star_named_expressions?
        case x, *rest:
            print("使用 star_named_expression 形式的主題表達式")
            print("第一個值:", x)
            print("其餘值:", rest)
        # 第二種情況:named_expression
        case y:
            print("使用 named_expression 形式的主題表達式")
            print("值:", y)

# 測試函式
# 使用第一種情況:star_named_expression "," star_named_expressions
process_data((1, 2, 3, 4, 5))

# 使用第二種情況:named_expression
process_data("hello")
使用 star_named_expression 形式的主題表達式
第一個值: 1
其餘值: [2, 3, 4, 5]
使用 named_expression 形式的主題表達式
值: hello

case_block

case_block: "case" patterns [guard] ':' block
guard: 'if' named_expression
  • "case":這是 case_block 的開始關鍵字,它表示了一個匹配情況的開始。

  • patterns:這是一個模式列表,用於匹配 subject_expr 中的值。這些模式可以是任何有效的 Python patterns(模式),包括常數、變數、列表、元組、字典等。

  • [guard]:這是可選的(optional)保護條件。如果存在保護條件,則在模式匹配成功後,將進一步檢查該保護條件。如果條件為真,則相應的代碼塊將被執行。

  • ":":這表示 case_block 的結束,並標誌著後面的代碼塊的開始。

  • block:這是與匹配情況相關聯的代碼塊。當模式匹配成功並且保護條件(如果存在)也為真時,將執行這個代碼塊。

def process_data(data):
    match data:
        # Match constant 匹配常量
        case 0:
            print("Data is zero")
        
        # Match range 匹配範圍
        case 1 | 2 | 5:  ## case 1, 2, 5:
            print("Data is either 1, 2, or 5")
        
        # Match variable 匹配變量
        case x if isinstance(x, int) and x % 2 == 0:
            print(f"Data is an even number: {x}")
        
        # Match sequence 匹配序列
        case [x, y]:
            print(f"Data is a pair: {x}, {y}")
        
        # Match dictionary 匹配字典
        case {'name': name, 'age': age}:
            print(f"Data is a person with name {name} and age {age}")

# 測試
process_data(0)
process_data(5)
process_data([1, 2])
process_data({'name': 'John', 'age': 30})
process_data(10)
Data is zero
Data is either 1, 2, or 5
Data is a pair: 1, 2
Data is a person with name John and age 30
Data is an even number: 10

patterns

patterns       ::=  open_sequence_pattern | pattern
pattern        ::=  as_pattern | or_pattern
closed_pattern ::=  | literal_pattern
                    | capture_pattern
                    | wildcard_pattern
                    | value_pattern
                    | group_pattern
                    | sequence_pattern
                    | mapping_pattern
                    | class_pattern

在上述 patterns 語法中:

  1. patterns:這是一個最上層的規則,表示模式的整體結構。它可以是一個開放的序列模式(open_sequence_pattern)或一個普通模式(pattern)。

  2. pattern:這是一個模式的基本單元,它可以是 作為模式(as_pattern)或模式(or_pattern)

  3. closed_pattern:這是模式的閉合形式,也就是不能再嵌套更多模式的形式。它可以是空的,或是下列任何一種模式:

    • literal_pattern:字面值模式,用於精確匹配特定值。
    • capture_pattern:捕獲模式,用於將變數綁定到匹配部分。
    • wildcard_pattern:萬用字元模式,表示不關心的值。
    • value_pattern:值模式,用於對特定類型的值進行匹配。
    • group_pattern:群組模式,用於將一組模式視為一個單元。
    • sequence_pattern:序列模式,用於對序列(如列表、元組)進行匹配。
    • mapping_pattern:映射模式,用於對映射(如字典)進行匹配。
    • class_pattern:類別模式,用於對類實例進行匹配。

這些模式組合在一起,提供了 Python 中 case 語法中的模式匹配功能。

以下為每一種 pattern 的實際範例:

  1. literal_pattern(字面值模式):這表示一個確切的值。在 case 中,如果變數的值與字面值模式匹配,則該 case 就會被執行。

    case 42:
        print("變數值是 42")
    
  2. capture_pattern(捕獲模式):這允許將變數捕獲為一個名稱,並將其與特定模式匹配的部分綁定在一起。

    case Point(x, y):
        print(f"座標是 ({x}, {y})")
    
  3. wildcard_pattern(萬用字元模式):這是一個省略符號,表示不關心變數的值。

    case _:
        print("不管是什麼值")
    
  4. value_pattern(值模式):這是一個特定的值,用於與變數進行比較。

    case 0:
        print("變數值為 0")
    
  5. group_pattern(群組模式):這允許將一組模式視為一個單元。

    case (Point(x1, y1), Point(x2, y2)):
        print(f"座標 1 是 ({x1}, {y1}),座標 2 是 ({x2}, {y2})")
    
  6. sequence_pattern(序列模式):這表示一個序列(如列表、元組等),並且可以在模式中使用序列解包。

    case [x, y, z]:
        print(f"第一個元素是 {x},第二個元素是 {y},第三個元素是 {z}")
    
  7. mapping_pattern(映射模式):這表示一個映射(如字典),並且可以使用在模式中對鍵值對進行匹配。

    case {'name': name, 'age': age}:
        print(f"姓名是 {name},年齡是 {age}")
    
  8. class_pattern(類別模式):這與捕獲模式類似,但專門用於對類實例進行匹配。

    case MyClass(value):
        print(f"類別中的值為 {value}")
    

這些是 PEP 634 中定義的各種模式,它們讓我們可以更靈活地在 case 語句中進行模式匹配。


pep 636 (官網教學)


GroqChat

Python 3.10 新增 STRUCTURAL PATTERN MATCHING(結構化模式匹配)功能,比傳統的 switch 作法更加強大,具有以下幾個優點:

  1. 模式(Pattern): 支援更多的模式比較,例如比較列表或字典的元素,或是使用正則表達式等。

  2. 萬用字元(Wildcards): 支援使用萬用字元 ( _* ) 來匹配任意值,可更彈性的模式匹配。

  3. 嵌套(Nesting): 支援嵌入,可處理 complex data structures。

  4. 型別(Type): 支援 Type Hints 來限制變數的型別,可以在編譯時就檢查型別的正確性。

  5. 可讀性(Readability): 相較於 if-elif-else 鏈或 switch-case 語句,structural pattern matching 可以使程式碼更加清晰易讀。

  6. 可靠性(Reliability): 相較於使用 if-elif-else 鏈或 switch-case 來比較值的相等性,structural pattern matching 可以更可靠地比較值的結構和類型。

  7. 可擴展性(Extensibility): 使用 Python 的 Type Hints,容易擴展和維護程式碼。

以下就上述優點,分別舉例示範:

from __future__ import annotations
from typing import Any

# 1. 支持更多的模式比較,例如比較列表或字典的元素,或是使用正則表達式等。
def compare_list(value: list[Any]) -> str:
    match value:
        case [first, *rest] if first == 'a':
            return f'List starts with "a" and has {len(rest)} more elements.'
        case [first, *rest] if first == 'b':
            return f'List starts with "b" and has {len(rest)} more elements.'
        case _:
            return 'List does not start with "a" or "b".'

# 2. 支持使用萬用字元(Wildcards): 支持使用萬用字元 (_ 或 *) 來匹配任意值,從而實現更加彈性的模式匹配。
def handle_value(value: Any) -> str:
    match value:
        case int(n) if n % 2 == 0:
            return f'Even number: {n}'
        case int(n) if n % 2 == 1:
            return f'Odd number: {n}'
        case str(s) if s.isdigit():
            return f'Digit string: {s}'
        case str(s):
            return f'String: {s}'
        case _:
            return 'Unknown value'

# 3. 支持嵌套的模式,從而可以更加輕鬆地處理 complex data structures。
def handle_complex_data(value: dict) -> str:
    match value:
        case {'name': str(name), 'age': int(age)}:
            return f'Name: {name}, Age: {age}'
        case {'name': str(name), 'address': {'city': str(city), 'state': str(state)}}:
            return f'Name: {name}, City: {city}, State: {state}'
        case _:
            return 'Unknown complex data structure'

# 4. 支持使用 Python 的 Type Hints 來限制變數的型別,從而可以在編譯時就檢查型別的正確性。
def handle_typed_value(value: float) -> str:
    match value:
        case float(n) if n.is_integer():
            return f'Integer: {int(n)}'
        case float(n):
            return f'Float: {n}'
        case _:
            return 'Unknown value'

# 5. 可讀性(Readability): 相較於 if-elif-else 鏈或 switch-case 語句,structural pattern matching 可以使程式碼更加清晰易讀。
def handle_readability(value: Any) -> str:
    if isinstance(value, int):
        if value % 2 == 0:
            return f'Even number: {value}'
        else:
            return f'Odd number: {value}'
    elif isinstance(value, str):
        if value.isdigit():
            return f'Digit string: {value}'
        else:
            return f'String: {value}'
    else:
        return 'Unknown value'

# 6. 可靠性(Reliability): 相較於使用 if-elif-else 來比較值的相等性,structural pattern matching 可以更加可靠地比較值的結構和類型。
def handle_reliability(value: Any) -> str:
    if isinstance(value, list):
        if value[0] == 'a':
            return 'List starts with "a".'
        else:
            return 'List does not start with "a".'
    else:
        return 'Not a list.'

# 7. 可擴展性(Extensibility): 由於使用了 Python 的 Type Hints,因此可以更加容易地擴展和維護程式碼。
def handle_extensibility(value: dict) -> str:
    match value:
        case {'name': str(name), 'age': int(age), **rest}:
            return f'Name: {name}, Age: {age}, Extra: {rest}'
        case _:
            return 'Unknown complex data structure'

if __name__ == '__main__':
    print(compare_list([1, 2, 3]))
    print(compare_list(['a', 2, 3]))
    print(compare_list(['b', 2, 3]))

    print(handle_value(123))
    print(handle_value(12345))
    print(handle_value('123'))
    print(handle_value('abcd'))

    print(handle_complex_data({'name': 'John', 'age': 30}))
    print(handle_complex_data({'name': 'Jane', 'address': {'city': 'New York', 'state': 'NY'}}))

    print(handle_typed_value(123.0))
    print(handle_typed_value(123.4))

    print(handle_readability(123))
    print(handle_readability('123'))
    print(handle_readability('abcd'))

    print(handle_reliability([1, 2, 3]))
    print(handle_reliability(['a', 2, 3]))
    print(handle_reliability('abcd'))

    print(handle_extensibility({'name': 'John', 'age': 30}))
    print(handle_extensibility({'name': 'Jane', 'age': 30, 'address': '123 Main St'}))