Python增强方案:结构化模式匹配

2020-06-24 06:12:34

Brandt Bucher<;Brandtbucher at gmail.com&>,Tobias Kohn<;kohnt at tobiaskohn.ch&>,Ivan Levkivskyi<;levkivskyi at gmail.com&>,Guido van Rossum<;Guido van Rossum<;Guido at python.org&>,Talin<;viridia at gmail.com>;

该PEP建议向Python添加模式匹配语句[1],以便创建更具表现力的方式来处理结构化异构数据。作者采取了整体的方法,提供了静态和运行时规范。

PEP275和PEP3103之前提出了类似的结构,但遭到拒绝。与其以优化IF为目标……。埃利夫..。Else语句(就像那些PEP一样),该设计侧重于泛化序列、映射和对象析构,它使用了PEP 617提供的语法特性,从而引入了更强大的解析Python源代码的方法。

让我们从一些轶事开始:isinstance()是大规模Python代码库中调用次数最多的函数之一(根据静态调用计数),特别是在分析某个数百万行生产代码库时,发现isinstance()是调用次数第二多的构建函数(仅次于len())。即使算上内建类,它仍然排在前十名。大多数这样的调用后面都有特定的属性访问。

异类数据的处理(即一个变量可以接受多种类型的值的情况)在现实世界的代码中很常见。

Python没有分解对象数据的表达方式(即将一个对象的内容分成多个变量)。

它在数字领域的成功表明,Python在处理同构数据时非常优秀。它还内置了对同构数据结构(如列表和数组)以及语义构造(如迭代器和生成器)的支持。

Python在构造对象时具有表现力和灵活性。它具有对集合文字和理解的语法支持。可以使用由特殊的__init__()方法定制的位置调用和关键字调用创建自定义对象。

此PEP旨在通过以模式匹配的形式添加对异构数据的专用语法支持来改进对它的支持。在非常高的级别上,它类似于正则表达式,但它可以匹配任意Python对象,而不是匹配字符串。

我们相信这将提高相关代码的可读性和可靠性。为了说明可读性的提高,让我们考虑一个来自Python标准库的实际示例:

def is_tuple(Node):if isinstance(node,Node)and node.Children==[LParen(),RParen()]:返回True return(isinstance(node,Node)and len(node.Children)==3 and isinstance(node.Children[0],Leaf)and isinstance(node.Children[1],Node)and isinstance(node.Children[2],Leaf)和node.Children[0].value=。

使用此PEP中建议的语法,可以将其重写如下。请注意,建议的代码将在不对此处的Node和其他类的定义进行任何修改的情况下工作:

def is_tuple(node:node)->;bool:Match node:case Node(Children=[LParen(),RParen()]):return True case Node(Children=[Leaf(Value=";(";),Node(),Leaf(Value=";)";)]):return True case_:return false。

类似于如何通过用户定义的__init__()方法定制构造对象,我们提出可以通过一种新的特殊的__Match__()方法定制析构对象。作为此PEP的一部分,我们指定了通用__Match__()API,它的Object.__Match__()实现,以及一些标准库类(包括PEP557数据类)。请参阅下面的运行时部分。

最后,我们的目标是为静态类型检查器和类似工具提供全面的支持。为此,我们建议引入一个@typeing.seed类装饰器,它在运行时是无操作的,但会向静态工具指示必须在同一模块中定义该类的所有子类。这将允许有效的静态穷举检查,并且与数据类一起,将为代数数据类型提供很好的支持[2]。有关更多详细信息,请参阅静态检查器部分。

总的来说,我们认为模式匹配在各种现代语言中已经被证明是一种有用的表达工具。特别是,这个PEP的许多方面都受到了Rust[3]和Scala[4]中模式匹配工作方式的启发。

.复合_语句:|IF_stmt.|Match_stmtMatch_stmt:";Match";Expression';:';换行缩进CASE_BLOCK+DEDENTcase_BLOCK:";CASE";Pattern[Guard]';:';BLOKGuard:';IF';expression Pattern:Name';:=';or_Pattern|or_Patternor_Pattern:Closed_Pattern)*Closed_Pattern:|名称_Pattern|文字模式|Constant_Pattern|GROUP_Pattern|SEQUENCE_Pattern|MAPPING_Pattern|class_Pattern。

我们建议匹配语法是语句,而不是表达式。虽然在许多语言中它是一个表达式,但作为语句更符合Python语法的一般逻辑。有关更多讨论,请参阅被拒绝的想法。允许的模式列表在下面的模式小节中指定。

建议将匹配词作为软关键字,以便在Match语句的开头将其识别为关键字,但允许在其他地方将其用作变量或参数名。

提出的选择匹配的大规模语义是选择第一个匹配模式并执行相应的套。其余的模式没有尝试过。如果没有匹配的模式,则语句将失败,并从下面的语句继续执行。

从本质上讲,这相当于一连串的如果……。埃利夫..。其他陈述。请注意,与前面建议的switch语句不同,预计算调度字典语义在这里不适用。

没有默认值或大小写-相反,可以使用特殊的通配符_(参见NAME_Pattern一节)作为最终的Catch-All';模式。

在成功的模式匹配期间进行的名称绑定比执行的套件持续时间更长,并且可以在Match语句之后使用。这遵循其他可以绑定名称的Python语句的逻辑,例如for loop和with Statement。例如:

匹配形状:案例点(x,y):.。大小写矩形(x,y,_,_):.print(x,y)#此操作有效

我们逐步引入所提出的语法。在这里,我们从主构建块开始。支持以下模式:

文字模式由简单的文字组成,如字符串、数字、布尔值或无:

匹配号:案例0:打印(";Nothing";)案例1:打印(";Just One";)案例2:打印(";一对夫妇";)案例-1:打印(";比什么都少";)案例1-1J:打印(";祝您好运.)。

文本模式使用与右侧的文本相等,因此在上面的示例中,将计算number==1,然后可能计算number==2。请注意,尽管技术上的负数是用一元减号表示的,但出于模式匹配的目的,它们被认为是文字。不允许一元加。二进制加和减只允许将实数和虚数相加以形成复数,如1+1j。

支持原始字符串和字节字符串。不允许使用F字符串(因为它们通常不是字面值)。

名称模式总是成功的。出现在作用域中的名称模式使该名称成为该作用域的本地名称。例如,如果采用";";CASE子句,则在上述代码片段后使用NAME可能会引发Unound LocalError而不是NameError:

匹配问候语:案例";";:Print(";Hello!";)案例名称:Print(f";Hi{name}!";)if name==";Santa";:#<;--可能引发Unbound LocalError.#但如果问候语不为空,则可以正常工作。

在匹配每个CASE子句时,一个名称最多只能绑定一次,出现名称重合的两个名称模式是错误的。在模式中,特殊的单下划线(_)名称是一个例外,它是一个永远不会绑定以下内容的通配符:

匹配数据:案例[x,x]:#错误!.。case[_,_]:Print(";Some Pair";)Print(_)#错误!

注意:仍然可以使用保护来匹配具有相等项的集合。此外,[x,y]|Point(x,y)是合法模式,因为这两个备选方案永远不会同时匹配。

它用于匹配常量和枚举值。使用普通Python名称解析规则查找模式中的每个带点的名称,并使用该值与匹配表达式进行相等比较(与文字相同)。作为避免名称模式歧义的特例,简单名称必须以点为前缀,以将其视为引用:

从枚举导入Enumclass Color(Enum):Black=1 red=2BLACK=1RED=2Match color:case.Black|Color.BLACK:Print(";Black适合每种颜色";)case Black:#这只会为黑色分配一个新值。..。

如果名称已加点,则可以省略前导圆点,但不禁止添加前导圆点,因此.Color.BLACK与Color.BLACK相同。请参阅考虑用于常量值模式的其他语法备选方案的被拒绝的想法。

初始名称不能是_,因为_在模式匹配中有特殊含义,因此这些名称无效:

但是,a._是合法的,并且像往常一样加载带有对象a的name_的属性。

序列模式遵循与解包赋值相同的语义,与解包赋值一样,可以使用类似元组和类似列表的语法,具有相同的语义。每个元素可以是任意模式;也可以最多有一个*名称模式来捕获所有剩余项目:

匹配集合:案例1,[x,*Other]:Print(";GET 1和嵌套序列";)案例(1,x):Print(f";GET 1和{x}";)。

要匹配序列模式,目标必须是Collection tions.abc.Sequence的实例,而不能是任何类型的字符串(str、bytes、bytearray)。它不能是迭代器。有关特定集合类的匹配信息,请参阅下面的类模式。

[";a";,*_,";z";]匹配以";a";开头并以";z";结尾的两个或更多长度的序列。

映射模式是可迭代解包到映射的推广,其语法类似于字典显示,但每个键和值都是Patterns";{";(Pattern";:";Pattern)+";}";。还允许使用**名称模式来提取剩余的项目。关键位置中只允许使用文字和常量值字符类型:

目标必须是集合的实例。abc.Mapping.Extra键会被忽略,即使**REST不存在。这与序列模式不同,在序列模式中,多余的项会导致匹配失败。但映射实际上与序列不同:它们有自然的结构子类型行为,即,将带有额外键的字典传递到某个地方可能会正常工作。

由于这个原因,**_在映射模式中是无效的;它将始终是可以删除而不会产生后果的anop。

匹配的键值对必须已存在于映射中,并且不是由__MISSING__或__GETIEM__动态创建的。例如,Collection tions.defaultdict实例将只匹配具有输入匹配块时已经存在的键的模式。

类模式支持分解任意对象。有两种可能的对象属性匹配方式:按类似位置的Point(1,2)匹配和像User(id=id,name=";Guest";)那样按名称匹配。这两项可以组合,但是位置匹配不能跟在名称匹配之后。类模式中的每一项都可以是任意模式。举个简单的例子:

匹配是否成功是通过在模式中命名的类(示例中为Point和Rectangle)上调用特殊的__Match__()方法来确定的,匹配值(Shape)是唯一的参数。如果该方法返回None,则匹配失败,否则匹配继续w.r.t。返回的代理对象的属性,请参见运行时部分中的详细信息。

命名类必须继承自类型。它可以是单个名称,也可以是带点的名称(例如,Some_mod.SomeClass或mod.pkg.Class)。前导名称不能是_,因此,例如_(.)。和_.c(.)。都是无效的。使用object(foo=_)检查匹配的对象是否具有属性foo。

此PEP仅为对象和一些内置和标准库类完全指定了__Match__()的行为,自定义类只需要遵循运行时部分中指定的协议。毕竟,类的作者最知道如何还原他们所写的逻辑。然后,运行库将链接这些调用,以允许与任意嵌套的模式进行匹配。

可以使用|将多个可选模式合并为一个。这意味着如果至少有一个备选模式匹配,则整个模式匹配;备选模式从左到右尝试,并且具有短路特性;如果匹配,则不尝试后续模式。例如:

匹配内容:大小写0|1|2:打印(";小数";)大小写[]|[_]:打印(";短序列";)大小写字符串()|字节():打印(";类似字符串";)大小写_:打印(";其他";)。

备选方案可以绑定变量,只要每个备选方案绑定相同的变量集(不包括_)。例如:

匹配内容:案例1|x:#错误!.。案例x|1:#错误!.。情况一:=[1]|情况二:=[2]:#错误!.。案例foo(arg=x)|bar(arg=x):#有效,双臂绑定.。案例[x]|x:#有效,双臂绑定.。

每个顶级模式后面都可以跟一个IF表达式形式的保护。如果模式匹配并且Guardevalue值为TRUE值,则CASE子句成功。例如:

匹配输入:case[x,y]if x>;max_int and y>;max_int:print(";get一对大数";)case x if x>;max_int:print(";get a Large number";)case[x,y]if x==y:print(";get equal Items";)case_:print(";不是未完成的输入";)。

如果评估保护引发异常,它将继续传播,而不是使CASE子句失败。出现在模式中的名字在守卫成功之前被绑定。所以这将会奏效:

Values=[0]匹配值:Case[x]如果x:.#这未执行case_:.print(X)#这将打印";0";

请注意,嵌套模式不允许使用保护,因此[x if x>;0]是一个语法错误,而1|2 if 3|4将被解析为(1|2)if(3|4)。

从可靠性的角度来看,经验表明,在处理一组可能的数据值时遗漏一个案例会导致难以调试的问题,从而迫使人们添加如下安全断言:

def get_first(data:Union[int,list[int]])->;int:if isinstance(data,list)and data:return data[0]elif isinstance(data,int):return data否则:assert false,";永远不会出现在这里";

PEP484规定静态类型检查器应该支持关于枚举值的穷举无条件检查。PEP586后来将此要求推广到文字类型。

此PEP进一步将此要求推广到任意模式。适用这种情况的典型情况是将表达式与联合类型匹配:

定义分类(val:Union[int,Tuple[int,int,int],list[int]])->;str:Match val:case[x,*Other]:返回f";以{x}";case[x,y]开头的序列,如果x&>;0和y>;0:返回f";一对{x}和{y}";case int():返回f";某个整数"。

从枚举导入枚举键入import Unionclass Level(Enum):Basic=1 Advanced=2 PRO=3class User:Name:Str Level:Levelclass Admin:Name:straccount:Union[User,Admin]Match Account:Case Admin(Name=Name)|User(Name=Name,Level=Level.PRO):.。案例用户(Level=Level.ADVANCED):.#类型检查错误:基本用户未处理。

显然,不需要可匹配的协议(就PEP544而言),因为每个类都是可匹配的,因此要接受上面指定的检查。

通常,希望对一组类应用穷举而不定义特别联合类型,如果联合定义中缺少某个类,这本身就是脆弱的。将一组类似记录的类组合成一个联合的设计模式在其他支持模式匹配的语言中很流行,其名称为代数数据类型[2]或ADT。

我们建议在tyingmodule[6]中添加一个特殊的修饰器类@Seed,它在运行时不会产生任何影响,但会向静态类型检查器表明,该类的所有子类(直接和间接)应该定义在与基类相同的模块中。

其想法是,由于所有子类都是已知的,类型检查器可以将密封的基类视为其所有子类的联合。与数据类相结合,这允许在Python中干净而安全地支持ADT。请考虑以下示例:

从数据类导入数据类通过键入import seal@sealedclass节点:.class表达式(节点):.class语句(节点):.@dataclassclass名称(表达式):名称:str@dataclassclass操作(表达式):左:表达式op:str右:expression@dataclassclass赋值(语句):target:str值:expression@dataclassesclass print(语句):value:expression。

有了这样的定义,类型检查器可以安全地将节点视为UNION[Name,Operation,Assignment,Print],并且还可以安全地将表达式视为UNION[Name,Operation]。因此,这将在以下代码片断中导致类型检查错误,因为不处理名称(并且类型检查器可以给出有用的错误消息):

def dump(node:node)->;str:匹配节点:案例分配(target,value):return f";{target}={dump(Value)}";case print(Value):return f";print({dump(Value)})";case Operation(Left,op,right):return f";({dump(Left)}{op}{dump(Right)})";

类模式容易受到运行时类型擦除的影响。也就是说,虽然可以定义类型别名IntQueue=Queue[int],以便像IntQueue()这样的模式在语法上是有效的,但是类型检查器应该拒绝这样的匹配:

请注意,上述代码片段实际上在运行时失败,因为当前实现了类型化模块中的泛型类,以及最近接受的PEP585中的内置泛型类,因为它们禁止isinstance检查。

澄清一下,泛型类一般不会被禁止参与模式匹配,只是它们的类型参数不能被明确指定。如果子模式或文字绑定了类型变量,仍然可以。例如:

从键入import Generic,TypeVar,UnionT=TypeVar(';T';)class Result(Generic[T]):First:t Other:List[T]Result:Union[Result[int],Result[str]]Match Result:CASE RESULT(FIRST=INT():.#结果的类型为RESULT[INT]HERE CASE RESULT(OTHER=[";Foo";,";bar";,*rest]):.#。

当用户错误地尝试将值与常量进行匹配而不是使用常量值模式时,名称模式始终是赋值目标这一事实可能会产生意想不到的后果。因此,在运行时,此类匹配将始终成功,而且会覆盖常量的值。因此,静态类型检查器应警告此类情况,这一点很重要。例如:

通过键入import FinalMAX_INT:FINAL=2**64value=0Match Value:case max_int:#此处的类型检查错误:无法分配给最终名称print(";GET BIG NUMBER";)case.MAX_INT:#This is OK Print(";GET BIG&34;)case_:Print(";其他";)。

类型检查器应该对模式匹配中的星型项执行精确的类型检查,给出它们异类List[T]类型,或者PEP589指定的TypedDict类型。例如:

Stuff:tuple[int,str,str,Float]匹配材料:case a,*b,0.5:#这里a是int,b是list[str].。

理想情况下,与之相比,Match语句应该具有良好的运行时性能。

..