Python 的 Pattern Matching

Python 的 Pattern Matching

假設你曾經寫過其它的程式語言,遇到這種一大串的 else if 組合的時候,通常會想到的是 switchcase...when 之類的語法。在 Python 並沒有這樣的設計,真的遇到一堆判斷就是用 if...elif...else... 寫就是了。這樣的設計是因為 Python 的作者認為 switch 語法並不是那麼的必要,而且 if...elif...else... 也可以達到相同的效果,所以就沒有加入 switch 語法。沒有 switchcase 的設計並沒什麼大不了的,事實上有些程式語言就算有 switchcase 的寫法,本質上也只是 if..else 而已。

match 比對

其實 switch 語法一堆人在敲碗,但碗都敲破了也沒有用,提議一直被否決,就覺得不太需要。不過在許多人的努力,在 PEP 上討論了好陣子,終於在 PEP 634 定案,並且 Python 3.10 實作了新的語法 match,寫起來的手感跟其它程式語言的 switch 語法有點像。match 語法的正式名稱叫做「結構化模式比對(Structural Pattern Matching)」,由於是在 3.10 版之後才有,所以太舊版本的 Python 是沒有支援這種寫法。假設我有原本個簡單流程判斷像這樣:

weather = "rain"

if weather == "rain":
    print("下雨天")
elif weather == "sunny":
    print("出太陽")
elif weather == "cloudy":
    print("多雲")
else:
    print("不知道")

使用 match 語法的話,可以寫成這樣:

weather = "rain"

match weather:
    case "rain":
        print("下雨天")
    case "sunny":
        print("出太陽")
    case "cloudy":
        print("多雲")
    case something:
        print(f"我不知道 {something} 是什麼天氣")

前面三個 case 分別是在比對字串(Python 會使用 == 來進行比對),如果比對成功的話就會結束整個比對的過程,而最後一個 case 是指如果上面所有的路線都比對失敗的話會走的路,差不多就是 else 的概念。在後面加上變數名稱的話表示比對到的值會被指派給這個變數。至於要用什麼名字倒是無所謂,但如果發現在這個 case 裡根本沒打算使用它的話,可以給它一個底線 _

weather = "rain"

match weather:
    case "rain":
        print("下雨天")
    case "sunny":
        print("出太陽")
    case "cloudy":
        print("多雲")
    case _:
        print("我根本不在乎是什麼天氣")

那個 _ 代表「I don't care」的意思。看到這裡,大家有覺得這樣語法看起來有比較清楚嗎?老實說我覺得並沒有差多少。

如果只是這樣的簡單比對的話,我也認同用 if..else.. 還比較直覺一點。但如果你以為 match 語法就只有這樣,那你就太小看 Pattern Matching 了,Pattern Matching 在其它程式語言可是大家搶著抄但又不一定抄的起來的好用設計。再舉個例子,假設我想判斷某個值是數字還是字串,用 if..else.. 寫起來大概像這樣:

value = True

if type(value) in [int, float]:
    print("數字")
elif type(value) == str:
    print("字串")
else:
    print("我不知道你是誰")

match 不只能比對簡單的值,還能比對型別,所以改用 match 寫的話:

data = 123

match data:
    case int() | float():
        print("整數")
    case str():
        print("字串")
    case _:
        print("其他型別")

在第一個 case 中間的那個 | 表示是「或者」的意思,雖然整個行數沒有變比較少,但程式碼的可讀性有比較好一點點。match 還能用來比對串列、字典、集合等等資料結構,在處理比較複雜的資料結構時挺方便的:

user_data = [1, "悟空", 18]

match user_data:
    case [id, name, age]:
        print(f"{id} - 我是 {name},我今年 {age} 歲")

    case _:
        print("你好")

user_data 裡面有三個值,透過 Pattern Matching 如果有「比對」到一樣的模式,被比對到的部份可以直接變成變數。也許你並不需要所有的變數,如果你只是想要得到資料裡的第一個值,剩下的不需要,可以用底線 _ 來代表不需要的部份:

user_data = [1, "悟空", 18]

match user_data:
    case [id, _, _]:
        print(f"會員編號:{id}")

    case _:
        print("hi")

這應該就不是一般的 if..else.. 或其它程式語言的 switch 能夠輕鬆做到的事了。

除了串列、Tuple 能比對,字典也能比對:

user_data = {"name": "Kitty", "gender": "female"}

match user_data:
    case {"gender": "male", "name": name}:
        print(f"{name} 先生您好!")

    case {"gender": "female", "name": name}:
        print(f"{name} 女士您好!")

    case _:
        print("您好!")

原本一般大概會使用 if..else.. 來判斷 gender 的值再決定怎麼打招呼,但透過 Pattern Matching 可以直接比對到字典裡 gender 的值,而且順序還不需要一樣,這樣的寫法看起來更直覺。Python 的 match 不只這樣,還能在 case 後面加上條件,像這樣:

number = [10, 0]

match number:
    case x, y if y != 0:
        print(x / y)

    case _:
        print("第二個數字不能為零!")

另外,match 不只一次比對一個,還能使用 * 符號一次比對多個,像這樣:

numbers = [1, 2, 3, 4, 5]

match numbers:
    case _, *others:
        print(others)

    case _:
        print("Hello Python")

第一個元素我刻意使用 _ 表示我不需要,但後面的元素可以使用 * 符號把它全部收下來,這個應該也是其它程式語言的 switch 辦不到的事。講了這麼多關於 match 的介紹,最後用一個簡單但算經驗的例子來做個收尾,大家不知道有沒有聽過「費氏數列(Fibonacci)」?這個數列的規則是:

這東西跟我們的年夜飯的概念有點像,大年初三吃的就是大年初一加上初二的總合。這個數列是很經典的「遞迴函數」的實作,用 match 可以寫的很簡潔易懂:

def fib(n):
    match n:
        case n if n < 2:
            return n
        case _:
            return fib(n - 1) + fib(n - 2)

print(fib(6))  # 第 6 項 = 8
print(fib(8))  # 第 8 項 = 21

Python 的 不像其它程式語言的 switch 只是 if..else.. 的變形或替代品,它還能用來比對或拆解更複雜的資料結構,在適當的地方使用 match 可以讓程式碼看起來更直覺、更易懂。

工商:想學 Python 嗎?我教你啊!

想要成為軟體工程師嗎?這不是條輕鬆的路,需要有足夠的決心、設定目標並持續學習,我們的 ASTROCamp 軟體工程師培訓營提供專業的前後端課程培訓,幫助你在最短時間內建立正確且扎實的軟體開發技能,有興趣而且不怕吃苦的話不妨來試試看吧 :)