初試 match/case

April 18, 2022

有的時候,會想列舉一些常數,作為流程操作的依據之一,例如,遊戲中操作人物時的上下左右:

STOP = 0
RIGHT = 1
LEFT = 2
UP = 3
DOWN = 4

列舉在 Python 還有更好的做法,不過就目前這系列文件的進度而言,先用以上的方式就可以了。

案例的對應流程

這時如果想依據變數 action 來決定該進行的流程,就目前已看過的流程語法而言,你可以使用〈if 分支判斷〉看過的 if/elif/else

STOP = 0
RIGHT = 1
LEFT = 2
UP = 3
DOWN = 4

action = RIGHT

if action == STOP:
    print('播放停止動畫')
elif action == RIGHT:
    print('播放向右動畫')
elif action == LEFT:
    print('播放向左動畫')
elif action == UP:
    print('播放向上動畫')
elif action == DOWN:
    print('播放向下動畫')
else:
    print('不支援此動作')

就 Python 而言,這是正確的解法之一,不過,如果你有使用過其他語言的經驗,應該會想問,Python 有沒有 C-like 之類語言的 switch 語法呢?沒有!

如果你真的不想使用 if/elif/else 來實現各案例的對應流程,依情況的不同,可以有不同的寫法,例如,就上例而言,可以透過 dict 改寫如下:

STOP = 0
RIGHT = 1
LEFT = 2
UP = 3
DOWN = 4

action = RIGHT

messages = {
    STOP:  '播放停止動畫',
    RIGHT: '播放向右動畫',
    LEFT:  '播放向左動畫',
    UP:    '播放向上動畫',
    DOWN:  '不支援此動作'
};

print(messages.get(action, '不支援此動作'))

沒有 switch!

雖然來自其他語言的開發者,總是會覺得這有點麻煩,覺得如果有 switch 之類的語法,不是寫來更直覺嗎?然而,Python 的圈子裡,一直以來就是沒有 switch 之類的語法,只不過其他語言幾乎都有 switch 之類的語句,因為「別人都有,就你沒有」的關係,使得 Python 沒有 switch 這件事,相對而言就像個異端。

因為其他語言幾乎都有,這就使得從其他語言來到 Python 的使用者,理所當然地,會想要有 switch 的方案,在 Python 社群中,也確實曾有提案與討論,而且歷史甚為悠久——早在 2001 年就提出了 PEP275,後來在 2006 年又有了 PEP3103,然而最終它們都被否決了。

基本上,否決的原因在於,在社群各式各樣的提案中,沒有一個實現可適切地融入 Python 既有的語法與風格。事實上,Python 之父 Guido van Rossum 在 PEP3103 也分析許多情境與方案,以及它們面對的問題,但都覺得過於複雜,若不使用 switch 的方案還比較單純,於是,其在 PEP3103 的最後結論就是「現在定案還太早了(It is too early to decide)」。

Guido 後來在 PyCon 2007 時做了個快速調查,就投票結果來聲稱 PEP3103 不受歡迎,合理正當地予以否決了。只不過,「為什麼沒有 switch?」這問題仍然被問到爛掉了。甚至,在 Python 官方的〈Design and History FAQ〉直接列出條目來回答這陳年問題,就結論而言,使用 if/elif/elsedict 完全可以應付需求,然而,乍看之下,很多人應該也會覺得這就是在說「Python 就是不需要 switch」!

match/case 模式比對

Python 3.10 加入了 match/case 語法,這是個很強大的語法,目的是用來支援〈模式比對(pattern matching)〉,只不過字面值(literal)也是其支援的比對模式之一,例如方才的例子可以改寫如下:

STOP = 0
RIGHT = 1
LEFT = 2
UP = 3
DOWN = 4

action = RIGHT

match action:
    case 0:
        print('播放停止動畫')
    case 1:
        print('播放向右動畫')
    case 2:
        print('播放向左動畫')
    case 3:
        print('播放向上動畫')
    case 4:
        print('播放向下動畫')
    case _:
        print('不支援此動作')

最後的 _ 是個萬用模式(wildcard pattern),代表任何模式,通常用於先前案例都不符合的情況下,該執行的流程。

這看來像是 switch,因此不少人會說,Pythton 終於加入了 swtich,這不是正確的說法,因為在 case 的部份,你必須明確地列出值的結構,也就是說,你不能如下撰寫:

STOP = 0
RIGHT = 1
LEFT = 2
UP = 3
DOWN = 4

action = RIGHT

match action:
    case STOP:
        print('播放停止動畫')
    case RIGHT:
        print('播放向右動畫')
    case LEFT:
        print('播放向左動畫')
    case UP:
        print('播放向上動畫')
    case DOWN:
        print('播放向下動畫')
    case _:
        print('不支援此動作')

這是因為上例第一個 case 沒有指定模式,這表示只要是值就符合,然後指定給新建立的 STOP 變數了,這個 STOP 與程式一開頭的 STOP 是不同的變數,也因為一開始的案例,只要是值就符合,後續的案例就沒有意義了,因此上例會顯示以下的錯誤訊息:

    case STOP:
         ^^^^
SyntaxError: name capture 'STOP' makes remaining patterns unreachable

字面值(literal)是 match/case 支援的比對模式之一,也就可以比對字串、listdict 字面值:

text = 'hi'

match text:
    case 'hi':
        print('嗨')
    case 'hello':
        print('哈囉')
        
lt = [3, 4]

match lt:
    case [1, 2]:
        print('三')
    case [3, 4]:
        print('七')
        
dt = {'x': 10, 'y': 20}

match dt:
    case {'x': 0, 'y': 0}:
        print('原點')
    case {'x': x, 'y': y}:
        print((x, y))

最後一個 dict 的例子,示範了如何將比對的值指定給變數,也就是對於其他的 dict,只要符合 'x''y' 作為鍵,對應的就會被指定給 xy 變數,以上的執行結果如下:

(10, 20)

想將年齡為 14 的人列出來嗎?

people = [
    ['Justin', 46],
    ['Monica', 43],
    ['Irene', 14]
];

for p in people:
    match p:
        case [name, 14]:
            print(name)

當然,就這個需求而言,if/else 就可以實現了,這只是用來示範如何在模式比對後,將想要的值指定給變數。

案例輔助條件

如果多個案例共用一個流程,也可以透過 | 來比對多個模式,也就是 or 模式:

score = int(input('輸入分數:'))

level = score // 10

match level:
    case 10 | 9:
        print('得 A')
    case 8:
        print('得 B')
    case 7:
        print('得 C')
    case 6:
        print('得 D')
    case _:
        print('不及格')

如果想知道 or 模式比對到結果是 10 還是 9 呢?可以使用 as

score = int(input('輸入分數:'))

level = score // 10

match level:
    case 10 | 9 as s:
        print('得 A')
        if s == 10:
            print('滿分!真棒!')
    case 8:
        print('得 B')
    case 7:
        print('得 C')
    case 6:
        print('得 D')
    case _:
        print('不及格')

如果需要對案例進行條件判斷,可以配合 if

dt = {'x': 10, 'y': 20}

match dt:
    case {'x': 0, 'y': 0}:
        print('原點')
    case {'x': x, 'y': y} if x * y > 0:
        print('一、三象限', (x, y))
    case {'x': x, 'y': y} if x * y < 0:
        print('二、四象限', (x, y))

match/case 語法在 PEP634 的全名是結構化模式比對(Structural Pattern Matching),而 match 是將具有特定結構的資料映射至值或動作,其結構就包含了型態、資料成員的名稱、順序、值等。

這也就是為什麼 match 可以搭配的,都是些能提供結構資訊的對象,像是數字、字串等字面值,listdict 等,日後你若會運用 namedtupledatatclass,必要時也可以搭配模式比對,或者可以配點關鍵字引數模式,或者針對類別實例定義 __match_args__ 列出結構資訊,也可以搭配 match/case

比對模式時的方式也非常多元化,以上只是大致列出幾個,詳細列出各種可用的模式沒有意義,日後有機會遇上再來使用吧!必要時詳閱 PEP634,看看是不是有適用的比對方式就可以了!

分享到 LinkedIn 分享到 Facebook 分享到 Twitter