close

前言

很多Python核心開發者都認為Python不需要添加switch-case這種語法,因為可以通過if/elif/else實現一樣的效果。事實上Guido本人也對這種語法不感冒,所以直到Python 3.10一個新的match-case才被加了進來。

這個新的語法中文叫做結構模式匹配(Structural Pattern Matching),由於新特性內容很多所以有三個PEP介紹它:

PEP 634 – Structural Pattern Matching: Specification: 介紹match語法和支持的模式PEP 635 – Structural Pattern Matching: Motivation and Rationale: 解釋語法這麼設計的理由PEP 636 – Structural Pattern Matching: Tutorial: 一個教程。介紹概念、語法和語義

switch-case 和 match-case的區別

拿一個小例子對比下。下面是通過HTTP CODE返回對應類型錯誤信息函數,在之前的例子中通過if判斷要這麼寫:

defhttp_error(status):ifstatus==400:return'Badrequest'elifstatus==401:return'Unauthorized'elifstatus==403:return'Forbidden'elifstatus==404:return'Notfound'else:return'Unknownstatuscode'

如果使用match-case語法:

defhttp_error(status):matchstatus:case400:return'Badrequest'case401:return'Unauthorized'case403:return'Forbidden'case404:return'Notfound'case_:return'Unknownstatuscode'

match後面跟要匹配的變量,case後面跟不同的條件,之後是符合條件需要執行的語句。最後一個case加下劃線表示缺省匹配,如果前面的條件沒有匹配上就跑到這個case裡面執行,相當於之前的else。

這其實是一個典型的switch-case用法,如果只是這樣,我也覺得確實沒必要添加這個新語法,一方面代碼沒有做到優化,一方面縮進反而更多了。

但是match-case語法能做的事情遠超C/Go這些語言裡的switch-case,它其實是Scala, Erlang等語言裡面的match-case,它支持複雜的模式匹配,接下來我會通過多個模式的例子詳細演示這個新的語法的靈活性和pythonic。

字面量(Literal)模式

上面的例子就是一個字面量模式,使用Python自帶的基本數據結構,如字符串、數字、布爾值和None:

matchnumber:case0:print('zero')case1:print('one')case2:print('two')捕捉(Capture)模式

可以匹配單個表達式的賦值目標。為了演示方便,每個例子都會放到函數中,把match後面的待匹配的變量作為參數(capture_pattern.py):

defcapture(greeting):matchgreeting:case"":print("Hello!")casename:print(f"Hi{name}!")ifname=="Santa":print('Match')

如果greeting非空,就會賦值給name,但是要注意,如果greeting為空會拋NameError或者UnboundLocalError錯誤,因為name在之前沒有定義過:

In:capture('Alex')HiAlex!In:capture('Santa')HiSanta!MatchIn:capture('')Hello!---------------------------------------------------------------------------UnboundLocalErrorTraceback(mostrecentcalllast)InputIn[4],in<cellline:1>()---->1capture('')InputIn[1],incapture(greeting)1defcapture(greeting):2matchgreeting:3case"":4print("Hello!")5casename:6print(f"Hi{name}!")---->7ifname=="Santa":8print('Match')UnboundLocalError:localvariable'name'referencedbeforeassignment序列(Sequence)模式

可以在match里使用列表或者元組格式的結果,還可以按照PEP 3132 – Extended Iterable Unpacking裡面使用first, *rest = seq模式來解包。我用一個例子來介紹:

In:defsequence(collection):...:matchcollection:...:case1,[x,*others]:...:print(f"Got1andanestedsequence:{x=},{others=}")...:case(1,x):...:print(f"Got1and{x}")...:case[x,y,z]:...:print(f"{x=},{y=},{z=}")...:In:sequence([1])In:sequence([1,2])Got1and2In:sequence([1,2,3])x=1,y=2,z=3In:sequence([1,[2,3]])Got1andanestedsequence:x=2,others=[3]In:sequence([1,[2,3,4]])Got1andanestedsequence:x=2,others=[3,4]In:sequence([2,3])In:sequence((1,2))Got1and2

注意,需要符合如下條件:

這個match條件第一個元素需要是1,否則匹配失敗
第一個case用的是列表和解包,第二個case用的是元組,其實和列表語義一樣,第三個還是列表

如果case後接的模式是單項的可以去掉括號,這麼寫:

defsequence2(collection):matchcollection:case1,[x,*others]:print(f"Got1andanestedsequence:{x=},{others=}")case1,x:print(f"Got1and{x}")casex,y,z:print(f"{x=},{y=},{z=}")

但是注意,其中case 1, [x, *others]是不能去掉括號的,去掉了解包的邏輯就變了,要注意。

通配符(Wildcard)模式

使用單下劃線_匹配任何結果,但是不綁定(不賦值到某個或者某些變量上)。一開始的例子:

defhttp_error(status):matchstatus:...#省略case_:return'Unknownstatuscode'

最後的case _就是通配符模式,當然還可以有多個匹配:

In:defwildcard(data):...:matchdata:...:case[_,_]:...:print('Somepair')...:In:wildcard(None)In:wildcard([1])In:wildcard([1,2])Somepair

在前面說到的序列模式也支持_:

In:defsequence2(collection):...:matchcollection:...:case["a",*_,"z"]:...:print('matchesanysequenceoflengthtwoormorethatstartswith"a"andendswith"z".')...:case(_,_,*_):...:print('matchesanysequenceoflengthtwoormore.')...:case[*_]:...:print('matchesasequenceofanylength.')...:In:sequence2(['a',2,3,'z'])matchesanysequenceoflengthtwoormorethatstartswith"a"andendswith"z".In:sequence2(['a',2,3,'b'])matchesanysequenceoflengthtwoormore.In:sequence2(['a','b'])matchesanysequenceoflengthtwoormore.In:sequence2(['a'])matchesasequenceofanylength.

使用通配符需求注意邏輯順序,把範圍小的放在前面,範圍大的放在後面,防止不符合預期。

恆定值(constant value)模式

這種模式主要匹配常量或者enum模塊的枚舉值:

In:classColor(Enum):...:RED=1...:GREEN=2...:BLUE=3...:In:classNewColor:...:YELLOW=4...:In:defconstant_value(color):...:matchcolor:...:caseColor.RED:...:print('Red')...:caseNewColor.YELLOW:...:print('Yellow')...:casenew_color:...:print(new_color)...:In:constant_value(Color.RED)#匹配第一個caseRedIn:constant_value(NewColor.YELLOW)#匹配第二個caseYellowIn:constant_value(Color.GREEN)#匹配第三個caseColor.GREENIn:constant_value(4)#常量值一樣都匹配第二個caseYellowIn:constant_value(10)#其他常量10

這裡注意,因為case具有綁定的作用,所以不能直接使用YELLOW這種常量,例如下面這樣:

YELLOW=4defconstant_value(color):matchcolor:caseYELLOW:print('Yellow')

這樣語法是錯誤的。

映射(Mapping)模式

其實就是case後支持使用字典做匹配:

In:defmapping(config):...:matchconfig:...:case{'sub':sub_config,**rest}:...:print(f'Sub:{sub_config}')...:print(f'OTHERS:{rest}')...:case{'route':route}:...:print(f'ROUTE:{route}')...:In:mapping({})In:mapping({'route':'/auth/login'})#匹配第一個caseROUTE:/auth/login##匹配有sub鍵的字典,值綁定到sub_config上,字典其他部分綁定到rest上In:mapping({'route':'/auth/login','sub':{'a':1}})#匹配第二個caseSub:{'a':1}OTHERS:{'route':'/auth/login'}類(Class)模式

case後支持任何對象做匹配。我們先來一個錯誤的示例:

In:classPoint:...:def__init__(self,x,y):...:self.x=x...:self.y=y...:In:defclass_pattern(obj):...:matchobj:...:casePoint(x,y):...:print(f'Point({x=},{y=})')...:In:class_pattern(Point(1,2))---------------------------------------------------------------------------TypeErrorTraceback(mostrecentcalllast)InputIn[],in<cellline:1>()---->1class_pattern(Point(1,2))InputIn[],inclass_pattern(obj)1defclass_pattern(obj):2matchobj:---->3casePoint(x,y):4print(f'Point({x=},{y=})')TypeError:Point()accepts0positionalsub-patterns(2given)

這是因為對於匹配來說,位置需要確定,所以需要使用位置參數來標識:

In:defclass_pattern(obj):...:matchobj:...:casePoint(x=1,y=2):...:print(f'match')...:In:class_pattern(Point(1,2))match

另外一個解決這種自定義類不用位置參數的匹配方案,使用__match_args__返回一個位置參數的數組,就像這樣:

In:classPoint:...:__match_args__=('x','y')...:...:def__init__(self,x,y):...:self.x=x...:self.y=y...:In:fromdataclassesimportdataclassIn:@dataclass...:classPoint2:...:x:int...:y:int...:In:defclass_pattern(obj):...:matchobj:...:casePoint(x,y):...:print(f'Point({x=},{y=})')...:casePoint2(x,y):...:print(f'Point2({x=},{y=})')...:In:class_pattern(Point(1,2))Point(x=1,y=2)In:class_pattern(Point2(1,2))Point2(x=1,y=2)

這裡的Point2使用了標準庫的dataclasses.dataclass裝飾器,它會提供__match_args__屬性,所以可以直接用。

組合(OR)模式

可以使用|將多個字面量組合起來表示或的關係,|可以在一個case條件內存在多個,表示多個或關係:

defor_pattern(obj):matchobj:case0|1|2:#0,1,2三個數字匹配print('smallnumber')caselist()|set():#列表或者集合匹配print('listorset')casestr()|bytes():#字符串或者bytes符合print('strorbytes')casePoint(x,y)|Point2(x,y):#借用之前的2個類,其中之一符合即可print(f'{x=},{y=}')case[x]|x:#列表且只有一個元素或者單個值符合print(f'{x=}')

這裡注意一下,由於匹配順序,case [x] | x這句中的[x]是不會被觸發的,另外x不能是集合、字符串、byte等類型,因為在前面的條件中會被匹配到不了這裡。我們試一下:

In:or_pattern(1)smallnumberIn:or_pattern(2)smallnumberIn:or_pattern([1])listorsetIn:or_pattern({1,2})listorsetIn:or_pattern('sss')strorbytesIn:or_pattern(b'sd')strorbytesIn:or_pattern(Point(1,2))x=1,y=2In:or_pattern(Point2(1,2))x=1,y=2In:or_pattern(4)x=4In:or_pattern({})x={}

另外在Python里是沒有表示AND關係的case語法的。

AS模式

AS模式在早期其實是海象(Walrus)模式,後來討論後發現使用as關鍵字可以讓這個語法更有優勢:

In:defas_pattern(obj):...:matchobj:...:casestr()ass:...:print(f'Gotstr:{s=}')...:case[0,int()asi]:...:print(f'Gotint:{i=}')...:case[tuple()astu]:...:print(f'Gottuple:{tu=}')...:caselist()|set()|dict()asiterable:...:print(f'Gotiterable:{iterable=}')...:...:In:as_pattern('sss')Gotstr:s='sss'In:as_pattern([0,1])Gotint:i=1In:as_pattern([(1,)])Gottuple:tu=(1,)In:as_pattern([1,2,3])Gotiterable:iterable=[1,2,3]In:as_pattern({'a':1})Gotiterable:iterable={'a':1}

需要註明一下,這裡面的[0, int() as i]是一種子模式,也就是在模式中包含模式:[0, int() as i]是case匹配的序列模式,而其中int() as i是子模式,是AS模式。

子模式在match語法裡面是可以靈活組合的。

向模式添加條件

另外模式還支持加入if判斷(叫做guard):

In:defgo(obj):...:matchobj:...:case['go',direction]ifdirectionin['east','north']:...:print('Rightway')...:casedirectionifdirection=='west':...:print('Wrongway')...:case['go',_]|_:...:print('Otherway')...:In:go(['go','east'])#匹配條件1RightwayIn:go('west')#匹配條件2WrongwayIn:go('north')#匹配默認條件OtherwayIn:go(['go','west'])#匹配默認條件Otherway

這樣可以讓匹配作進一步判斷,相當於實現了某個程度的AND效果.

代碼目錄

本文代碼可以在mp項目找到

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()