Python 的 Pattern Matching
credit: Midjourney

Python 的 Pattern Matching

約 3,629 字

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

  • 第 1 項是 0
  • 第 2 項是 1
  • 第 3 項開始每一項都是前兩項的和,所以這個數列 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ... 以此類推。

這東西跟我們的年夜飯的概念有點像,大年初三吃的就是大年初一加上初二的總合。這個數列是很經典的「遞迴函數」的實作,用 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 可以讓程式碼看起來更直覺、更易懂。

工商服務

線上課程:五倍學院線上課程

合作夥伴

留言討論