PEP 642 – 结构化模式匹配的显式模式语法
- 作者:
- Alyssa Coghlan <ncoghlan at gmail.com>
- BDFL代表:
- 讨论列表:
- Python-Dev 列表
- 状态:
- 已拒绝
- 类型:
- 标准跟踪
- 依赖:
- 634
- 创建日期:
- 2020年9月26日
- Python版本:
- 3.10
- 更新历史:
- 2020年10月31日,2020年11月8日,2021年1月3日
- 决议:
- Python-Dev 消息
摘要
本PEP涵盖了PEP 634结构化模式匹配的另一种语法提案,该提案要求在所有捕获模式和值约束上使用显式前缀。它还为实例属性模式提出了一种新的专用语法,使其与提议的映射模式语法更加一致。
虽然结果必然比PEP 634中提出的语法更冗长,但它仍然比现状少得多。
例如,以下匹配语句将从一个2项序列、一个具有“host”和“port”键的映射、任何具有“host”和“port”属性的对象或一个“host:port”字符串中提取“host”和“port”详细信息,在后三种情况下将“port”视为可选
port = DEFAULT_PORT
match expr:
case [as host, as port]:
pass
case {"host" as host, "port" as port}:
pass
case {"host" as host}:
pass
case object{.host as host, .port as port}:
pass
case object{.host as host}:
pass
case str{} as addr:
host, __, optional_port = addr.partition(":")
if optional_port:
port = optional_port
case __ as m:
raise TypeError(f"Unknown address format: {m!r:.200}")
port = int(port)
从高层次来看,本PEP建议将不同的可用模式类型分类如下
- 通配符模式:
__
- 组模式:
(PTRN)
- 值约束模式
- 相等约束:
== EXPR
- 身份约束:
is EXPR
- 相等约束:
- 结构约束模式
- 序列约束模式:
[PTRN, as NAME, PTRN as NAME]
- 映射约束模式:
{EXPR: PTRN, EXPR as NAME}
- 实例属性约束模式:
CLS{.NAME, .NAME: PTRN, .NAME == EXPR, .NAME as NAME}
- 类定义约束模式:
CLS(PTRN, PTRN, **{.NAME, .NAME: PTRN, .NAME == EXPR, .NAME as NAME})
- 序列约束模式:
- OR模式:
PTRN | PTRN | PTRN
- AS模式:
PTRN as NAME
(省略模式意味着__
)
这种方法的意图是
- 允许开发和发布模式匹配的初始形式,而无需预先决定处理裸名、属性查找和字面值的最佳默认选项
- 确保模式匹配在抽象语法树级别明确定义,允许模式匹配的语义规范和表面语法清晰分离
- 定义一个清晰简洁的“鸭子类型”语法,该语法可以潜在地在普通表达式中采用,作为更轻松地从同一对象检索包含多个属性的元组的一种方法
相对于PEP 634,该提案还故意消除了任何“绑定到右侧”而无需使用as
关键字(在PEP 634的映射模式和类模式中使用捕获模式)或在同一模式中绑定到左右两侧(在PEP 634中使用带有AS模式的捕获模式)的语法。
与其他PEP的关系
本PEP既依赖于PEP 634,又与之竞争——PEP作者同意匹配语句将是对语言的足够有价值的补充,值得学习过程中增加的额外复杂性,但不同意“简单名称与字面量或属性查找”真的在匹配模式中提供了名称绑定和值查找操作之间足够的语法区别(至少对于Python而言)。
本PEP同意PEP 640的精神(即,选择的跳过名称绑定的通配符模式应该在任何地方都受支持,而不仅仅是在匹配模式中),但现在建议使用不同的通配符语法拼写(__
而不是?
)。因此,它与PEP 640(按原样编写)存在竞争关系,但将补充一个提案,即弃用__
作为普通标识符,而是将其变成一个通用通配符标记,始终跳过创建新的局部变量绑定。
虽然尚未作为PEP提出,但Mark Shannon有一个PEP前草稿[8],表达了对PEP 634中模式匹配提案的运行时语义的若干担忧。本PEP在某种程度上与之互补,因为即使本PEP主要涉及表面语法更改而不是主要的语义更改,它也建议使抽象语法树定义更加明确,以更好地将表面语法的细节与代码生成步骤的语义分开。该PEP前草稿中有一个具体的想法,本PEP明确拒绝了:不同类型的匹配是互斥的想法。同一个值完全有可能匹配不同类型的结构模式,哪个模式优先将有意由匹配语句中case的顺序决定。
动机
最初的PEP 622(后来拆分为PEP 634、PEP 635和PEP 636)在其语法设计中包含了一个未陈述但必不可少的假设:普通表达式或现有的赋值目标语法都不能为匹配模式中使用的语法提供充分的基础。
虽然PEP没有明确说明这个假设,但其中一位PEP作者在python-dev上清楚地解释了它[1]
我看到的实际问题是,我们在这里有不同的文化/直觉从根本上发生冲突。特别是,许多程序员将模式匹配视为“扩展的switch语句”,因此发现名称是绑定而不是用于比较的表达式很奇怪。其他人认为它与当前的赋值语句不一致,并质疑为什么点分名称_不_是绑定。然而,所有群体似乎都有一点共同点,那就是他们指的是他们对新的匹配语句的理解和解释为“一致”或“直观”——自然地指出了我们作为PEP作者在设计中出错的地方。但关键在于:至少在Python世界中,本PEP提议的模式匹配是一种前所未有、全新的解决常见问题的方法。它不仅仅是已经存在事物的扩展。更糟糕的是:在设计PEP时,我们发现无论从哪个角度出发,你都会遇到看似“不一致”的问题(也就是说,模式匹配不能以有意义的方式简化为现有功能的“线性”扩展):总有一些东西从根本上超越了Python中已经存在的事物。这就是为什么我认为基于什么“直观”或“一致”的论点在这种情况下_没有意义_。
然后,本PEP的第一次迭代是为了证明第二个断言不准确,并且匹配模式可以被视为赋值目标的一种变体,而不会导致内在的矛盾。(之前提交的将此选项列在原始PEP 622的“被拒绝的想法”部分的PR之前已被拒绝[2])。
然而,针对此 PEP 的审查过程强烈表明,Tobias 在其电子邮件中提到的矛盾不仅存在,而且令人担忧,足以对PEP 634中提出的语法提案产生怀疑。因此,此 PEP 进行了修改,其范围甚至超过了PEP 634,并在很大程度上放弃了序列匹配语法与现有可迭代解包语法之间的对齐(实际上是对 DLS’20 论文[9]中提出的第一个问题做出了“不完全是,至少就确切的语法而言”的回答:“我们可以扩展像可迭代解包这样的功能使其适用于更通用的对象和数据布局吗?”)。
这导致了 PEP 目标的彻底反转:PEP 不再试图强调赋值和模式匹配之间的相似性,而是试图确保完全不重用赋值目标语法,从而降低了根据现有构造的经验对新构造做出错误推断的可能性。
最后,在完成提案的第三次迭代(完全删除了推断模式)之前,PEP 作者花了很多时间思考PEP 20中的以下条目
- 显式优于隐式。
- 特殊情况不足以打破规则。
- 面对歧义,拒绝猜测的诱惑。
如果我们从显式语法开始,我们总是可以以后添加语法快捷方式(例如,考虑最近提出的仅在使用原始更冗长形式多年的经验后为Union
和Optional
类型提示添加快捷方式的提案),而如果我们一开始只使用缩写形式,那么我们就没有办法在将来的版本中重新审视这些决策。
规范
此 PEP 保留了来自PEP 634的整体match
/case
语句结构和语义,但提出了多项更改,这意味着用户意图在具体语法中被明确指定,而不是需要从模式匹配上下文中推断出来。
在提议的抽象语法树中,语义也始终是明确的,不需要任何推断。
匹配语句
表面语法
match_stmt: "match" subject_expr ':' NEWLINE INDENT case_block+ DEDENT
subject_expr:
| star_named_expression ',' star_named_expressions?
| named_expression
case_block: "case" (guarded_pattern | open_pattern) ':' block
guarded_pattern: closed_pattern 'if' named_expression
open_pattern:
| as_pattern
| or_pattern
closed_pattern:
| wildcard_pattern
| group_pattern
| structural_constraint
抽象语法
Match(expr subject, match_case* cases)
match_case = (pattern pattern, expr? guard, stmt* body)
规则star_named_expression
、star_named_expressions
、named_expression
和block
是标准 Python 语法的一部分。
开放模式是指由多个标记组成的模式,并且不一定会以结束分隔符结尾(例如,__ as x
、int() | bool()
)。为了避免人类读者产生歧义,它们的用法仅限于顶级模式和组模式(即用括号括起来的模式)。
封闭模式是指由单个标记组成的模式(即__
),或者其语法中需要包含结束分隔符(例如,[as x, as y]
、object{.x as x, .y as y}
)。
与PEP 634一样,match
和case
关键字是软关键字,即它们在其他语法上下文中不是保留字(包括如果预期位置没有冒号时,在行的开头)。这意味着它们仅在匹配语句或 case 块的一部分时才被识别为关键字,并且允许在所有其他上下文中用作变量或参数名称。
与PEP 634不同,模式在抽象语法树中被明确定义为一种新的节点类型 - 即使表面语法与现有的表达式节点共享,解析器也会发出不同的抽象节点。
作为上下文,match_stmt
是表面语法中compound_statement
的新替代方案,而Match
是抽象语法中stmt
的新替代方案。
匹配语义
此 PEP 在很大程度上保留了PEP 634中提出的整体模式匹配语义。
模式的提议语法发生了重大变化,并在下面详细讨论。
还有一些关于类定义约束(PEP 634中的类模式)语义的提议更改,以消除对任何内置类型的特殊情况的需要(相反,引入专门用于实例属性约束的语法允许将这些内置类型所需的特性指定为适用于将__match_args__
设置为None
的任何类型)。
守卫
此 PEP 保留了PEP 634中提出的保护子句语义。
但是,语法略有更改,要求当存在保护子句时,case 模式必须是封闭模式。
这使读者更清楚地了解模式在哪里结束以及保护子句在哪里开始。(这主要是在 OR 模式中存在的一个潜在问题,其中保护子句看起来像是最终模式中条件表达式的开头。实际上这样做是非法语法,因此就编译器而言没有歧义,但是这种区别对于人类读者来说可能并不那么清晰)
不可反驳的case块
相对于PEP 634,此 PEP 中不可反驳的 case 块的定义略有变化,因为捕获模式不再作为与 AS 模式不同的概念存在。
除了这个警告之外,不可反驳的 case 的处理方式与PEP 634中相同
- 通配符模式是不可反驳的
- 其左侧不可反驳的 AS 模式
- 包含至少一个不可反驳模式的 OR 模式
- 带括号的不可反驳模式
- 如果一个 case 块没有保护子句并且其模式是不可反驳的,则该 case 块被认为是不可反驳的。
- 一个 match 语句最多可以有一个不可反驳的 case 块,并且它必须是最后一个。
模式
模式的顶级表面语法如下
open_pattern: # Pattern may use multiple tokens with no closing delimiter
| as_pattern
| or_pattern
as_pattern: [closed_pattern] pattern_as_clause
or_pattern: '|'.simple_pattern+
simple_pattern: # Subnode where "as" and "or" patterns must be parenthesised
| closed_pattern
| value_constraint
closed_pattern: # Require a single token or a closing delimiter in pattern
| wildcard_pattern
| group_pattern
| structural_constraint
如上所述,开放模式的用法仅限于顶级 case 子句以及在组模式中用括号括起来时。
模式的抽象语法明确指示哪些元素是子模式,哪些元素是子表达式或标识符
pattern = MatchAlways
| MatchValue(matchop op, expr value)
| MatchSequence(pattern* patterns)
| MatchMapping(expr* keys, pattern* patterns)
| MatchAttrs(expr cls, identifier* attrs, pattern* patterns)
| MatchClass(expr cls, pattern* patterns, identifier* extra_attrs, pattern* extra_patterns)
| MatchRestOfSequence(identifier? target)
-- A NULL entry in the MatchMapping key list handles capturing extra mapping keys
| MatchAs(pattern? pattern, identifier target)
| MatchOr(pattern* patterns)
AS模式
表面语法
as_pattern: [closed_pattern] pattern_as_clause
pattern_as_clause: 'as' pattern_capture_target
pattern_capture_target: !"__" NAME !('.' | '(' | '=')
(注意:右侧的名称不能是__
。)
抽象语法
MatchAs(pattern? pattern, identifier target)
AS 模式将as
关键字左侧的封闭模式与主题进行匹配。如果失败,则 AS 模式失败。否则,AS 模式将主题绑定到as
关键字右侧的名称并成功。
如果没有给出要匹配的模式,则隐含通配符模式(__
)。
为了避免与通配符模式混淆,双下划线(__
)不允许作为捕获目标(这就是!"__"
表达的意思)。
捕获模式总是成功的。它使用PEP 572中为命名表达式建立的名称绑定作用域规则将主题值绑定到名称。(摘要:除非存在适用的nonlocal
或global
语句,否则该名称成为最接近的包含函数作用域中的局部变量。)
在给定的模式中,给定的名称只能绑定一次。例如,这禁止case [as x, as x]: ...
,但允许case [as x] | (as x)
作为开放模式,AS 模式的用法仅限于顶级 case 子句以及在组模式中用括号括起来时。但是,几个结构约束允许在相关位置使用pattern_as_clause
将匹配主题的提取元素绑定到局部变量。这些主要在抽象语法树中表示为MatchAs
节点,除了序列模式中专用的MatchRestOfSequence
节点。
OR模式
表面语法
or_pattern: '|'.simple_pattern+
simple_pattern: # Subnode where "as" and "or" patterns must be parenthesised
| closed_pattern
| value_constraint
抽象语法
MatchOr(pattern* patterns)
当两个或多个模式由竖线(|
)分隔时,这称为 OR 模式。(单个简单模式就是如此)
只有最终的子模式可能是不可反驳的。
每个子模式必须绑定相同的名称集。
OR 模式依次将每个子模式与主题进行匹配,直到一个成功。然后,OR 模式被认为是成功的。如果没有任何子模式成功,则 OR 模式失败。
子模式大多需要是封闭模式,但对于值约束可以省略括号。
值约束
表面语法
value_constraint:
| eq_constraint
| id_constraint
eq_constraint: '==' closed_expr
id_constraint: 'is' closed_expr
closed_expr: # Require a single token or a closing delimiter in expression
| primary
| closed_factor
closed_factor: # "factor" is the main grammar node for these unary ops
| '+' primary
| '-' primary
| '~' primary
抽象语法
MatchValue(matchop op, expr value)
matchop = EqCheck | IdCheck
规则primary
在标准 Python 语法中定义,并且仅允许由单个标记组成的表达式,或者必须以结束分隔符结尾的表达式。
值约束替换了PEP 634的文字模式和值模式。
相等约束写成== EXPR
,而同一性约束写成is EXPR
。
如果主题值与右侧给定的值相等,则相等约束成功,而同一性约束仅在它们是完全相同的对象时才成功。
要比较的表达式在很大程度上仅限于单个标记(例如,名称、字符串、数字、内置常量),或者必须以结束分隔符结尾的表达式。
也允许使用高优先级一元运算符,因为感知歧义的风险很低,并且能够在没有括号的情况下指定负数是可取的。
如果同一个约束表达式在同一个匹配语句中出现多次,解释器可能会缓存计算出的第一个值并重复使用它,而不是重复执行表达式求值。(对于PEP 634的值模式,此缓存严格绑定到给定匹配语句的给定执行。)
与PEP 634中的字面量模式不同,本 PEP 要求复杂字面量必须用括号括起来才能被解析器接受。有关此点的讨论,请参阅“延迟想法”部分。
如果本 PEP 被优先于PEP 634采用,则所有字面量和值模式将以更明确的方式编写为值约束。
# Literal patterns
match number:
case == 0:
print("Nothing")
case == 1:
print("Just one")
case == 2:
print("A couple")
case == -1:
print("One less than nothing")
case == (1-1j):
print("Good luck with that...")
# Additional literal patterns
match value:
case == True:
print("True or 1")
case == False:
print("False or 0")
case == None:
print("None")
case == "Hello":
print("Text 'Hello'")
case == b"World!":
print("Binary 'World!'")
# Matching by identity rather than equality
SENTINEL = object()
match value:
case is True:
print("True, not 1")
case is False:
print("False, not 0")
case is None:
print("None, following PEP 8 comparison guidelines")
case is ...:
print("May be useful when writing __getitem__ methods?")
case is SENTINEL:
print("Matches the sentinel by identity, not just value")
# Matching against variables and attributes
from enum import Enum
class Sides(str, Enum):
SPAM = "Spam"
EGGS = "eggs"
...
preferred_side = Sides.EGGS
match entree[-1]:
case == Sides.SPAM: # Compares entree[-1] == Sides.SPAM.
response = "Have you got anything without Spam?"
case == preferred_side: # Compares entree[-1] == preferred_side
response = f"Oh, I love {preferred_side}!"
case as side: # Assigns side = entree[-1].
response = f"Well, could I have their Spam instead of the {side} then?"
请注意== preferred_side
示例:在约束表达式上使用显式前缀标记消除了仅对属性或字面量进行值查找的限制。
== (1-1j)
示例说明了如何使用括号将任何子表达式转换为封闭表达式。
通配符模式
表面语法
wildcard_pattern: "__"
抽象语法
MatchAlways
通配符模式总是成功的。与PEP 634一样,它不绑定任何名称。
在PEP 634选择下划线作为其通配符模式以与其他语言保持一致的情况下,本 PEP 选择双下划线,因为这有一条更清晰的路径,可以使整个语言的模式保持一致,而这条路径被"_"
由于与 i18n 相关的用例而阻塞。
示例用法
match sequence:
case [__]: # any sequence with a single element
return True
case [start, *__, end]: # a sequence with at least two elements
return start == end
case __: # anything
return False
组模式
表面语法
group_pattern: '(' open_pattern ')'
有关open_pattern
的语法,请参见上面的模式。
带括号的模式没有额外的语法,并且在抽象语法树中没有表示。它允许用户在模式周围添加括号以强调预期的分组,并在语法需要封闭模式时允许嵌套开放模式。
与PEP 634不同,序列模式不存在潜在的歧义,因为本 PEP 要求所有序列模式都用方括号编写。
结构约束
表面语法
structural_constraint:
| sequence_constraint
| mapping_constraint
| attrs_constraint
| class_constraint
注意:单独的“结构约束”子类别不用于抽象语法树,它仅用作表面语法定义中的一个方便的分组节点。
结构约束是用于对复杂对象进行断言并从中提取值的模式。
这些模式都可以绑定多个值,可以通过使用嵌套的 AS 模式,或者通过使用模式定义中包含的pattern_as_clause
元素来实现。
序列约束
表面语法
sequence_constraint: '[' [sequence_constraint_elements] ']'
sequence_constraint_elements: ','.sequence_constraint_element+ ','?
sequence_constraint_element:
| star_pattern
| simple_pattern
| pattern_as_clause
star_pattern: '*' (pattern_as_clause | wildcard_pattern)
simple_pattern: # Subnode where "as" and "or" patterns must be parenthesised
| closed_pattern
| value_constraint
pattern_as_clause: 'as' pattern_capture_target
抽象语法
MatchSequence(pattern* patterns)
MatchRestOfSequence(identifier? target)
序列约束允许检查序列中的项并可选地提取它们。
如果主题值不是collections.abc.Sequence
的实例,则序列模式失败。如果主题值是str
、bytes
或bytearray
的实例,则也会失败(有关可能删除此特殊情况的需求的讨论,请参阅“延迟想法”)。
序列模式最多可以包含一个星号子模式。星号子模式可以出现在任何位置,并在 AST 中使用MatchRestOfSequence
节点表示。
如果没有星号子模式,则序列模式是固定长度序列模式;否则它是可变长度序列模式。
如果主题序列的长度不等于子模式的数量,则固定长度序列模式失败。
如果主题序列的长度小于非星号子模式的数量,则可变长度序列模式失败。
主题序列的长度是使用内置的len()
函数(即通过__len__
协议)获取的。但是,解释器可以像为值约束表达式描述的那样以类似的方式缓存此值。
固定长度序列模式将子模式与主题序列的对应项匹配,从左到右。一旦子模式失败,匹配就会停止(失败)。如果所有子模式都成功匹配其对应的项,则序列模式成功。
可变长度序列模式首先将前导非星号子模式与主题序列的对应项匹配,就像固定长度序列一样。如果成功,则星号子模式将匹配由剩余主题项组成的列表,其中从末尾删除的项对应于星号子模式后面的非星号子模式。然后,剩余的非星号子模式与对应的主题项匹配,就像固定长度序列一样。
子模式大多需要是封闭模式,但对于值约束,可以省略括号。序列元素也可以在没有括号的情况下无条件捕获。
注意:在PEP 634允许与赋值语句中的可迭代解包相同的语法灵活性时,本 PEP 将序列模式专门限制为方括号形式。鉴于开放和带括号的形式比方括号更受欢迎用于可迭代解包,这有助于强调可迭代解包和序列匹配**不是**同一个操作。它还避免了带括号的形式在单元素序列模式和组模式之间的歧义问题。
映射约束
表面语法
mapping_constraint: '{' [mapping_constraint_elements] '}'
mapping_constraint_elements: ','.key_value_constraint+ ','?
key_value_constraint:
| closed_expr pattern_as_clause
| closed_expr ':' simple_pattern
| double_star_capture
double_star_capture: '**' pattern_as_clause
(注意,本语法故意不允许使用**__
,因为默认情况下会忽略其他映射条目)
closed_expr 在上面值约束下定义。
抽象语法
MatchMapping(expr* keys, pattern* patterns)
映射约束允许检查序列中的键和值,并可选地提取值。
如果主题值不是collections.abc.Mapping
的实例,则映射模式失败。
如果映射模式中给出的每个键都存在于主题映射中,并且每个键的模式都与主题映射的对应项匹配,则映射模式成功。
使用get
方法的两个参数形式和一个唯一的哨兵值来检查键的存在,这提供了以下好处:
- 查找过程中不需要创建异常。
- 实现
__missing__
的映射(例如collections.defaultdict
)仅匹配它们已包含的键,它们不会隐式添加键。
映射模式不能包含重复的键值。如果在检查映射模式时检测到重复的键,则该模式被认为无效,并引发ValueError
。虽然理论上可以在编译时检查重复的常量键,但目前没有定义或实现此类检查。
(注意:此语义描述源自PEP 634参考实现,该实现与编写时的PEP 634规范文本不同。该实现似乎合理,因此修改 PEP 文本似乎是解决差异的最佳方法。)
如果存在'**' as NAME
双星号模式,则该名称将绑定到一个dict
,其中包含主题映射中任何剩余的键值对(如果没有任何其他键值对,则该字典将为空)。
映射模式最多可以包含一个双星号模式,并且它必须位于最后。
值子模式大多需要是封闭模式,但对于值约束,可以省略括号(仍然需要:
键/值分隔符以确保条目看起来不像普通的比较操作)。
映射值也可以使用KEY as NAME
形式无条件捕获,无需括号或:
键/值分隔符。
实例属性约束
表面语法
attrs_constraint:
| name_or_attr '{' [attrs_constraint_elements] '}'
attrs_constraint_elements: ','.attr_value_pattern+ ','?
attr_value_pattern:
| '.' NAME pattern_as_clause
| '.' NAME value_constraint
| '.' NAME ':' simple_pattern
| '.' NAME
抽象语法
MatchAttrs(expr cls, identifier* attrs, pattern* patterns)
实例属性约束允许检查实例的类型并可选地提取属性。
实例属性约束不能重复相同的属性名称多次。尝试这样做会导致语法错误。
如果主题不是name_or_attr
的实例,则实例属性模式失败。这是使用isinstance()
进行测试的。
如果name_or_attr
不是内置type
的实例,则会引发TypeError
。
如果没有属性子模式,则如果isinstance()
检查成功,则约束成功。否则
- 每个给定的属性名称都将在主题上作为属性查找。
- 如果这引发了除
AttributeError
之外的异常,则异常会冒泡。- 如果这引发了
AttributeError
,则约束失败。- 否则,与关键字关联的子模式将与属性值匹配。如果未指定子模式,则假定使用通配符模式。如果失败,则约束失败。如果成功,则匹配继续到下一个属性。
- 如果所有属性子模式都成功,则整个约束成功。
实例属性约束允许通过使用object
作为所需的实例类型来实现鸭子类型检查(例如case object{.host as host, .port as port}:
)。
此处提出的语法也可能用作在单个赋值语句中从对象实例检索多个属性的新语法的基础(例如host, port = addr{.host, .port}
)。有关此点的进一步讨论,请参阅“延迟想法”部分。
类定义约束
表面语法
class_constraint:
| name_or_attr '(' ')'
| name_or_attr '(' positional_patterns ','? ')'
| name_or_attr '(' class_constraint_attrs ')'
| name_or_attr '(' positional_patterns ',' class_constraint_attrs] ')'
positional_patterns: ','.positional_pattern+
positional_pattern:
| simple_pattern
| pattern_as_clause
class_constraint_attrs:
| '**' '{' [attrs_constraint_elements] '}'
抽象语法
MatchClass(expr cls, pattern* patterns, identifier* extra_attrs, pattern* extra_patterns)
类定义约束允许在类上指定一系列常见属性并按位置检查,而不是需要在每个相关的匹配模式中指定属性名称。
对于实例属性模式
- 如果主题不是
name_or_attr
的实例,则类定义模式失败。这是使用isinstance()
进行测试的。 - 如果
name_or_attr
不是内置type
的实例,则会引发TypeError
。
无论是否存在任何参数,都会使用等效于getattr(cls, "__match_args__", _SENTINEL))
的代码检查主题是否有__match_args__
属性。
如果这引发了异常,则异常会冒泡。
如果返回的值不是列表、元组或None
,则转换失败,并在运行时引发TypeError
。
这意味着只有实际定义了__match_args__
的类型才能在类定义的模式中使用。未定义__match_args__
的类型仍然可以在实例属性模式中使用。
如果__match_args__
为None
,则只允许单个位置子模式。尝试以位置方式或使用双星号语法指定其他属性模式将在运行时引发TypeError
。
然后,此位置子模式与整个主题匹配,允许将类型检查与另一个匹配模式组合(例如,检查容器的类型和内容,或数字的类型和值)。
如果__match_args__
是列表或元组,则类定义的约束将转换为实例属性约束,如下所示
- 如果只有双星号属性约束子模式存在,则匹配过程就像对于等效的实例属性约束一样。
- 如果位置子模式的数量超过
__match_args__
的长度(使用len()
获取),则会引发TypeError
。 - 否则,位置模式
i
将转换为属性模式,使用__match_args__[i]
作为属性名称。 - 如果
__match_args__
中的任何元素不是字符串,则会引发TypeError
。 - 一旦位置模式转换为属性模式,则将其与双星号属性约束子模式中给出的任何属性约束组合,并像对于等效的实例属性约束一样进行匹配。
注意:此 PEP 中的__match_args__ is None
处理替换了PEP 634中bool
、bytearray
、bytes
、dict
、float
、frozenset
、int
、list
、set
、str
和tuple
的特殊情况处理。但是,这些类型的优化快速路径在实现中保留。
设计讨论
在匹配模式中要求显式限定简单名称
此 PEP 的第一个迭代版本接受了PEP 634的基本前提,即可迭代解包语法将为定义模式匹配的新语法提供良好的基础。
然而,在审查过程中,突出了两个主要问题和一个次要的歧义问题,这些问题直接源于该核心假设
- 最成问题的是,当默认情况下将简单名称绑定扩展到PEP 634提出的类模式语法时,
ATTR=TARGET_NAME
构造在不使用as
关键字的情况下向右绑定,并使用正常的赋值到左侧符号(=
)来实现! - 当默认情况下将简单名称绑定扩展到PEP 634提出的映射模式语法时,
KEY: TARGET_NAME
构造在不使用as
关键字的情况下向右绑定 - 将PEP 634捕获模式与 AS 模式(
TARGET_NAME_1 as TARGET_NAME_2
)一起使用会导致奇怪的“同时绑定到左侧和右侧”的行为
此 PEP 的第三次修订通过放弃与可迭代解包语法的对齐来解决此问题,而是要求所有对裸简单名称的使用(除了变量查找之外)都必须由前面的符号或关键字限定
as NAME
:局部变量绑定.NAME
:属性查找== NAME
:变量查找is NAME
:变量查找- 任何其他用法:变量查找
这种方法的主要好处是它使模式中简单名称的解释成为一项本地活动:前导as
表示名称绑定,前导.
表示属性查找,其他任何内容都是变量查找(无论我们是在读取子模式还是子表达式)。
使用此 PEP 中现在提出的语法,上面确定的问题案例不再读起来很糟糕
.ATTR as TARGET_NAME
比ATTR=TARGET_NAME
更明显地表示绑定KEY as TARGET_NAME
比KEY: TARGET_NAME
更明显地表示绑定(as TARGET_NAME_1) as TARGET_NAME_2
比TARGET_NAME_1 as TARGET_NAME_2
更明显地表示两个绑定
抵制猜测的诱惑
PEP 635查看了其他语言中模式匹配的使用方式,并尝试使用这些信息对模式匹配在 Python 中的使用方式做出合理的预测
- 希望将值提取到局部名称中可能比希望与存储在局部名称中的值匹配更常见
- 希望通过相等性进行比较可能比希望通过同一性进行比较更常见
- 即使用户无法在没有阅读文档或有人告诉他们的情况下自己弄清楚,他们也可能至少能够记住裸名称绑定值和属性引用查找值
需要明确的是,我认为这些预测实际上是合理的。但是,我认为我们也不需要事先猜测这一点:我认为我们可以从一个更明确的语法开始,该语法要求用户使用前缀标记(as
、==
或is
)来陈述他们的意图,然后根据模式匹配在 Python 中的实际使用情况在几年后重新评估情况。
在那时,我们将能够在至少以下选项中进行选择
- 确定显式语法足够简洁,并且不更改任何内容
- 为
None
、...
、True
和False
中的一个或多个添加推断的身份约束 - 为其他文字(可能包括复杂文字)添加推断的相等性约束
- 为属性查找添加推断的相等性约束
- 为裸名称添加推断的相等性约束或推断的捕获模式
所有这些想法都可以根据其自身的优点单独考虑,而不是成为首先引入模式匹配的潜在障碍。
如果最终引入了任何这些语法快捷方式,它们也易于根据底层更显式语法进行解释(前导as
、==
或is
将仅由解析器推断,而无需用户显式提供)。在实现级别,只需要更改解析器,因为可以重用现有的 AST 节点。
与局部变量中属性查找缓存的交互
此 PEP 和PEP 634之间的一个主要变化是使用== EXPR
进行相等性约束查找,而不是只提供NAME.ATTR
。最初的动机是为了避免与常规赋值目标的语义冲突,其中NAME.ATTR
已在赋值语句中用于设置属性,因此,如果NAME.ATTR
是符号值匹配的唯一语法,那么我们就会先发制人地排除将来任何尝试使用现有赋值语句语法对单个模式进行匹配的尝试。目前的动机更多地是关于普遍希望避免猜测用户的意图,而是要求他们在语法中明确地陈述意图。
但是,即使在匹配语句本身中,用于值模式的name.attr
语法也与局部变量赋值存在不良的交互,其中对于任何其他 Python 语句在语义上为中性的常规重构在应用于PEP 634样式的匹配语句时会引入重大的语义更改。
考虑以下代码
while value < self.limit:
... # Some code that adjusts "value"
属性查找可以安全地从循环中提取出来,并且只执行一次
_limit = self.limit:
while value < _limit:
... # Some code that adjusts "value"
使用此 PEP 中基于标记前缀的语法提案,值约束将同样容忍将匹配模式重构为使用局部变量而不是属性查找,以下两个语句在功能上是等效的
match expr:
case {"key": == self.target}:
... # Handle the case where 'expr["key"] == self.target'
case __:
... # Handle the non-matching case
_target = self.target
match expr:
case {"key": == _target}:
... # Handle the case where 'expr["key"] == self.target'
case __:
... # Handle the non-matching case
相反,当使用PEP 634省略标记前缀的值和捕获模式语法时,以下两个语句根本不会等效
# PEP 634's value pattern syntax
match expr:
case {"key": self.target}:
... # Handle the case where 'expr["key"] == self.target'
case _:
... # Handle the non-matching case
# PEP 634's capture pattern syntax
_target = self.target
match expr:
case {"key": _target}:
... # Matches any mapping with "key", binding its value to _target
case _:
... # Handle the non-matching case
此 PEP 确保在采用这种简单的重构风格时保留原始语义:使用== name
强制将结果解释为值约束,使用as name
进行名称绑定。
PEP 634关于仅提供简写语法,不提供显式前缀形式的提案意味着提供的首要答案是“好吧,别那样做,只对比命名空间中的属性,不要对比简单名称”。
PEP 622 的海象运算符语法存在另一个奇怪的交互行为,即它可能不会绑定与 case 子句主体中完全相同的海象运算符表达式相同的对象,但PEP 634 通过将海象运算符模式替换为 AS 模式修复了这种差异(其中,绑定到 RHS 上名称的值可能与 LHS 返回的值不相同,这是所有使用“as”关键字的标准特性)。
使用现有的比较运算符作为值约束前缀
如果接受专用值约束前缀的好处,那么下一个问题就是询问该前缀究竟应该是什么。
本 PEP 最初发布的版本提议使用之前未使用的 ?
符号作为等式约束的前缀,以及 ?is
作为同一性约束的前缀。在审查 PEP 时,Steven D’Aprano 提出了一项有说服力的反建议 [5],建议使用现有的比较运算符(==
和 is
)代替。
在 PEP 的初始迭代中,有一些关于使用 ==
作为前缀的担忧,这使得它没有被选为前缀
- 对于常见用例,它甚至比
?
更加视觉上嘈杂,因为许多接受过 PEP 8 训练的美学意识的人会想要在它和后面的表达式之间加上一个空格,有效地将其变成一个 3 个字符的前缀而不是 1 个字符 - 在映射模式中使用时,需要在
:
键/值分隔符和==
前缀之间留一个空格,否则标记器将错误地将其拆分(得到:=
和=
而不是:
和==
) - 在 OR 模式中使用时,需要在
|
模式分隔符和==
前缀之间留一个空格,否则标记器将错误地将其拆分(得到|=
和=
而不是|
和==
) - 如果在 PEP 634 样式的类模式中使用,需要在
=
关键字分隔符和==
前缀之间留一个空格,否则标记器将错误地将其拆分(得到==
和=
而不是=
和==
)
Steven 提出的解决这种冗长问题的方案不是引入一个全新的符号,而是保留在语法上明确的情况下省略前缀标记符的能力。
虽然在提案的第二次修订中接受了省略前缀标记符的想法,但在第三次修订中由于模糊性问题而再次放弃了。取而代之的是,以下几点适用
- 对于类模式,其他语法更改允许将等式约束写成
.ATTR == EXPR
,并将同一性约束写成.ATTR is EXPR
,这两种写法都非常易于阅读 - 对于映射模式,额外的语法噪声只是被容忍了(至少目前是这样)
- 对于 OR 模式,额外的语法噪声只是被容忍了(至少目前是这样)。但是,成员资格约束 可能会为减少将 OR 模式与等式约束结合的需要提供未来的途径(相反,要检查的值将被收集为集合、列表或元组)。
鉴于这种观点,PEP 635 反对将 ?
作为模式匹配语法的一部分的论点也适用于本提案,因此 PEP 进行了相应修订。
使用__
作为通配符模式标记
PEP 635 有力地论证了引入 ?
*仅* 作为通配符模式标记符是一个坏主意。由于值约束的语法已更改为使用现有的比较运算符而不是 ?
和 ?is
,因此该论点也适用于本 PEP。
但是,正如 Thomas Wouters 在 [6] 中指出的那样,PEP 634 选择 _
仍然存在问题,因为它可能意味着匹配模式将与 Python 的所有其他部分存在*永久*差异 - 在软件国际化和交互式提示符中使用 _
意味着实际上没有可行的途径将其用作通用“跳过绑定”标记符。
__
是从 Stack Overflow 答案 [7](最初由本 PEP 的作者发布)中提取的另一种“此值不需要”标记符,该答案讨论了现有 Python 代码中 _
的各种含义。
本 PEP 还提议采用一种实现技术,该技术将 __
的相关特殊情况的范围限制在解析器:定义一个新的 AST 节点类型(MatchAlways
)专门用于通配符标记符,而不是将其作为 Name
节点传递到 AST。
在解析器中,__
仍然表示在匹配模式中的常规名称或通配符标记符,具体取决于您在解析树中的位置,但在编译器的其余部分,Name("__")
仍然是正常的变量名,而 MatchAlways()
在匹配模式中始终是通配符标记符。
与 _
不同,__
没有其他用例,这意味着存在一条可行的途径,可以通过使 __
在 Python 中的任何地方都表示“跳过此名称绑定”来恢复与语言其余部分的标识符处理一致性
- 在解释器本身中,弃用加载名为
__
的变量。这将使读取__
发出弃用警告,而写入它最初将保持不变。为了避免减慢所有名称加载的速度,这可以通过编译器为弃用的名称发出额外的代码来处理,而不是在标准名称加载操作码中使用运行时检查。 - 在适当数量的版本发布后,更改解析器以对所有用作赋值目标的
__
的用法发出新的SkippedBinding
AST 节点,并相应地更新编译器的其余部分 - 考虑使
__
成为真正的硬关键字而不是软关键字
这条弃用路径无法用于 _
,因为解释器无法区分在名义上用作“不关心”标记符时尝试读回 _
,以及合法地将 _
作为 i18n 文本翻译函数或作为交互式提示符中的最后一个语句结果读取。
以双下划线开头的名称也已被保留供语言使用,无论是编译时常量(即 __debug__
)、特殊方法还是类属性名称混淆,因此在此处使用 __
将与这种现有方法保持一致。
在抽象语法树中显式表示模式
PEP 634 没有明确讨论如何在抽象语法树中表示匹配语句,而是将此细节留待作为实现的一部分进行定义。
因此,虽然 PEP 634 的参考实现绝对有效(并构成本 PEP 参考实现的基础),但它确实包含一个重大的设计缺陷:尽管 PEP 635 中的注释指出模式应被视为与表达式不同,但参考实现继续在 AST 中将它们表示为表达式节点。
结果是一个根本不抽象的 AST:应该以完全不同的方式编译的节点(因为它们是模式而不是表达式)以相同的方式表示,并且实现语言的类型系统(例如 CPython 的 C)无法提供任何帮助来跟踪哪些子节点应该是普通表达式,哪些应该是子模式。
本 PEP 并没有继续使用这种方法,而是定义了 AST 中一个新的显式“模式”节点,这使得可以在 AST 本身中明确定义模式及其允许的子节点,使实现新功能的代码更清晰,并允许 C 编译器在代码生成器处理模式还是表达式时提供更多帮助。
这种实现方法的更改实际上与本 PEP 中提出的表面语法更改是正交的,因此即使 PEP 的其余部分被拒绝,它仍然可以被采用。
对序列模式的更改
本 PEP 对相对于 PEP 634 的序列模式做了一个值得注意的更改
- 仅支持序列模式的方括号形式。不支持打开(无分隔符)或元组样式(括号作为分隔符)的序列模式。
相对于PEP 634,序列模式也受到更改的重大影响,该更改要求显式限定捕获模式和值约束,这意味着case [a, b, c]:
必须改为写成case [as a, as b, as c]:
,而case [0, 1]:
必须改为写成case [== 0, == 1]:
。
由于序列模式的语法不再直接从可迭代解包的语法派生而来,因此不再有意义地保留最初语法提案中纯粹为了与可迭代解包保持一致而包含的语法灵活性。
允许开放式和元组风格的序列模式并没有提高表达能力,只会增加意图的歧义(尤其是相对于组模式),并鼓励读者将模式匹配语法视为与赋值目标语法内在地相关联(PEP 634 的作者多次声明这不是读者希望走的路,而本 PEP 的作者现在也同意这种观点,尽管最初不同意)。
对映射模式的更改
本 PEP 对映射模式相对于PEP 634 做了两个显著的更改。
- 值捕获写成
KEY as NAME
,而不是KEY: NAME
- 允许更广泛的键:任何“封闭表达式”,而不仅仅是字面量和属性引用。
如上所述,第一个更改是确保所有使用目标名称绑定到子表达式或模式右侧的操作都使用as
关键字。
第二个更改主要是通过重用现有的表达式处理机制来简化解析器和代码生成器代码。对封闭表达式的限制旨在帮助减少关于键表达式在哪里结束以及匹配模式在哪里开始的歧义。这主要允许PEP 634 允许的超集,除了复杂字面量必须用括号括起来(至少目前是这样)。
将PEP 635 的映射模式示例改编为本 PEP 中提出的语法。
match json_pet:
case {"type": == "cat", "name" as name, "pattern" as pattern}:
return Cat(name, pattern)
case {"type": == "dog", "name" as name, "breed" as breed}:
return Dog(name, breed)
case __:
raise ValueError("Not a suitable pet")
def change_red_to_blue(json_obj):
match json_obj:
case { 'color': (== 'red' | == '#FF0000') }:
json_obj['color'] = 'blue'
case { 'children' as children }:
for child in children:
change_red_to_blue(child)
作为参考,等效的PEP 634 语法。
match json_pet:
case {"type": "cat", "name": name, "pattern": pattern}:
return Cat(name, pattern)
case {"type": "dog", "name": name, "breed": breed}:
return Dog(name, breed)
case _:
raise ValueError("Not a suitable pet")
def change_red_to_blue(json_obj):
match json_obj:
case { 'color': ('red' | '#FF0000') }:
json_obj['color'] = 'blue'
case { 'children': children }:
for child in children:
change_red_to_blue(child)
对类模式的更改
本 PEP 对类模式相对于PEP 634 做了几个显著的更改。
- 放弃了与类实例化的语法对齐,因为它具有误导性和无帮助性。相反,引入了一种新的专用语法来检查其他属性,该语法借鉴了映射模式而不是类实例化。
- 引入了一种新的专用语法用于简单的鸭子类型检查,适用于任何类。
- 各种内置类型和标准库类型的特殊情况得到了补充,通过通用检查
__match_args__
属性是否存在且值为None
。
如上所述,第一个更改有两个目的。
- 它是确保所有使用目标名称绑定到子表达式或模式右侧的操作都使用
as
关键字的一部分。使用=
分配到右侧尤其成问题。 - 它是确保模式中所有简单名称的使用都具有指示其目的的前缀(在本例中,前导
.
指示属性查找)的一部分。
与类实例化的语法对齐也被认为总体上没有帮助,因为类模式是关于将模式与属性进行匹配,而类实例化是关于将调用参数与类构造函数中的参数进行匹配,这可能与生成的实例属性没有太大关系。
第二个更改旨在使在 Python 中已经很常见的“鸭子类型”样式检查更容易使用模式匹配。
然后,这些模式的具体语法提案源于将实例视为属性名称到值的映射,并将属性查找语法(.ATTR
)与映射模式语法{KEY: PATTERN}
结合起来,得到cls{.ATTR: PATTERN}
。
允许cls{.ATTR}
与cls{.ATTR: __}
具有相同含义是考虑到前导.
足以使名称使用变得明确(它显然是属性引用,而与映射模式中的变量键进行匹配则可能存在歧义)。
最终的更改只是通过使其成为类在未定义__match_args__
时获得的默认行为来补充PEP 634 参考实现中仅限于 CPython 内部的一个检查(保留了PEP 634 中命名的内置类型和标准库类型的优化快速路径)。
改编PEP 635 中链接的类匹配示例表明,对于纯位置类匹配,主要影响来自值约束和名称绑定的更改,而不是来自类匹配的更改。
match expr:
case BinaryOp(== '+', as left, as right):
return eval_expr(left) + eval_expr(right)
case BinaryOp(== '-', as left, as right):
return eval_expr(left) - eval_expr(right)
case BinaryOp(== '*', as left, as right):
return eval_expr(left) * eval_expr(right)
case BinaryOp(== '/', as left, as right):
return eval_expr(left) / eval_expr(right)
case UnaryOp(== '+', as arg):
return eval_expr(arg)
case UnaryOp(== '-', as arg):
return -eval_expr(arg)
case VarExpr(as name):
raise ValueError(f"Unknown value of: {name}")
case float() | int():
return expr
case __:
raise ValueError(f"Invalid expression value: {repr(expr)}")
作为参考,等效的PEP 634 语法。
match expr:
case BinaryOp('+', left, right):
return eval_expr(left) + eval_expr(right)
case BinaryOp('-', left, right):
return eval_expr(left) - eval_expr(right)
case BinaryOp('*', left, right):
return eval_expr(left) * eval_expr(right)
case BinaryOp('/', left, right):
return eval_expr(left) / eval_expr(right)
case UnaryOp('+', arg):
return eval_expr(arg)
case UnaryOp('-', arg):
return -eval_expr(arg)
case VarExpr(name):
raise ValueError(f"Unknown value of: {name}")
case float() | int():
return expr
case _:
raise ValueError(f"Invalid expression value: {repr(expr)}")
类模式语法本身的更改在检查命名属性并提取其值而不依赖于__match_args__
时更相关。
match expr:
case object{.host as host, .port as port}:
pass
case object{.host as host}:
pass
将其与PEP 634 等效项进行比较,在该等效项中,不清楚哪些名称指的是匹配主题的属性,哪些名称指的是局部变量。
match expr:
case object(host=host, port=port):
pass
case object(host=host):
pass
在这种特定情况下,歧义无关紧要(因为属性和变量名称相同),但在一般情况下,知道哪个是哪个对于正确推理正在读取的代码至关重要。
延迟的想法
推断值约束
如上所述,本 PEP 并不排除将来添加推断的相等和同一性约束的可能性。
对于字面量,这些可能特别有价值,因为许多具有不言而喻含义的“魔术”字符串和数字很可能直接写入匹配模式,而不是存储在命名变量中。(例如None
之类的常量,或者0
和1
之类的明显特殊数字,或者其内容与任何变量名称一样具有描述性的字符串,而不是对像739452
这样的不透明数字进行神秘的检查)。
使某些必需的括号可选
PEP 目前严重偏向于在出现潜在歧义时要求使用括号。
但是,在许多情况下,它至少可以说走得太远了,主要涉及具有显式模式的 AS 模式。
在任何需要封闭模式的位置,AS 模式可能以双括号开头,因为嵌套模式也需要是封闭模式:((OPEN PTRN) as NAME)
由于子模式需要是封闭的,因此在许多这些情况下(例如序列模式子模式),直接接受CLOSED_PTRN as NAME
应该是合理的。
对这一点的进一步考虑已被推迟,因为使必需的括号可选是向后兼容的更改,因此以后可以根据具体情况放宽限制。
接受复杂字面量作为封闭表达式
PEP 634 的参考实现在解析器和编译器的其余部分中包含大量二元运算符的特殊情况,以便接受复杂字面量而不接受对字面值的任意二元数值运算。
理想情况下,这个问题将在解析器层解决,解析器直接发出一个用复数预填充的 Constant AST 节点。如果情况如此,那么可以通过类似于任何其他字面量的机制来接受复杂字面量。
然而,这不是处理复杂字面量的方式。相反,它们作为常规BinOp
节点传递到 AST,然后 AST 上的常量折叠传递将它们解析为具有复数值的Constant
节点。
为了让解析器直接解析复杂字面量,编译器需要能够告诉标记生成器为虚数生成不同的标记类型(例如INUMBER
),然后解析器就可以分别处理NUMBER + INUMBER
和NUMBER - INUMBER
,而不是其他二元运算。
或者,可以定义一个新的ComplexNumber
AST 节点类型,这将允许解析器通知后续的编译器阶段特定节点应该专门是一个复杂字面量,而不是一个任意二元运算。然后解析器可以接受NUMBER + NUMBER
和NUMBER - NUMBER
用于该节点,同时让ComplexNumber
的 AST 验证确保字面量的实部和虚部分别是实数和虚数,如预期的那样。
目前,本 PEP 已推迟处理此问题,而是只要求将复杂字面量括在括号中才能用于值约束和作为映射模式键。
允许在匹配模式中使用否定约束
使用本 PEP 中提出的语法,不允许写!= expr
或is not expr
作为匹配模式。
这两种形式都具有明确的潜在解释,即作为否定相等约束(即x != expr
)和否定同一性约束(即x is not expr
)。
然而,目前尚不清楚这两种形式是否会频繁出现到足以证明专用语法的合理性,因此可能的扩展已被推迟,等待社区进一步积累使用 match 语句的经验。
允许在匹配模式中使用成员资格检查
用于相等性和同一性约束的语法可以很容易地扩展到成员资格检查:in container
。
本 PEP 和PEP 634中提出的方案的一个缺点是,在同一个 case 中检查多个值的方式与 Python 中现有的任何容器成员资格检查都不一样。
# PEP 634's literal patterns
match value:
case 0 | 1 | 2 | 3:
...
# This PEP's equality constraints
match value:
case == 0 | == 1 | == 2 | == 3:
...
根据本 PEP 允许推断相等性约束只会使其看起来像PEP 634中的示例,它仍然不像等效的 if
语句头 (if value in {0, 1, 2, 3}:
)。
成员资格约束将提供一种更明确但仍然简洁的方式来检查匹配主体是否存在于容器中,并且它看起来与普通的包含检查相同。
match value:
case in {0, 1, 2, 3}:
...
case in {one, two, three, four}:
...
case in range(4): # It would accept any container, not just literal sets
...
此功能还可以轻松扩展以允许所有类型的 case 子句,而无需任何进一步的语法更新,只需在自定义类定义上适当地定义 __contains__
即可。
然而,虽然这似乎是一个有用的扩展,并且是解决本 PEP 在将多个相等性检查组合成 OR 模式时的冗长问题的好方法,但它对于使 match 语句成为对语言的宝贵补充并不是必不可少的,因此将其推迟到单独的提案中似乎更合适,而不是将其包含在此处。
为实例属性约束推断默认类型
实例属性约束的专用语法意味着可以从 object{.ATTR}
中省略 object
以得到 {.ATTR}
,而不会引入任何语法歧义(如果没有给出类,则会隐含 object
,就像在类定义中隐含基类列表一样)。
但是,目前尚不清楚节省六个字符是否值得使映射模式与实例属性模式在视觉上更难以区分,因此允许这样做已被推迟,作为将来可能考虑的主题。
避免序列模式中的特殊情况
本 PEP 和PEP 634中的序列模式目前将 str
、bytes
和 bytearray
特殊情况处理为永远不匹配序列模式。
如果我们要为这些类型定义一个新的 collections.abc.AtomicSequence
抽象基类,则可以删除此特殊情况处理,其中它们在概念上是一个项目,但仍然实现序列协议以允许随机访问其组成部分。
从实例检索多个属性的表达式语法
实例属性模式语法的设计使得它可以作为从对象中用单个表达式检索多个属性的通用语法的基础。
host, port = obj{.host, .port}
类似于切片语法仅允许在括号下标内使用,用于命名属性的 .attr
语法仅允许在大括号下标内使用。
模式匹配要变得有用并不需要这个想法,所以它不是本 PEP 的一部分。但是,它被提到了,作为一个可能的途径,使模式匹配感觉更融入语言的其余部分,而不是永远存在于它自己的完全分离的世界中。
从实例检索多个属性的表达式语法
如果大括号下标语法被接受用于实例属性模式匹配,然后随后扩展为提供对多个属性的通用提取,那么它可以进一步扩展以允许根据用于映射模式匹配的语法从容器中检索多个项目。
host, port = obj{"host", "port"}
first, last = obj{0, -1}
同样,模式匹配要变得有用并不需要这个想法,所以它不是本 PEP 的一部分。但是,与检索多个属性一样,它被包含在内,作为提议的模式匹配语法激发使对象分解更容易的想法的一个示例。
被拒绝的想法
限制值约束和映射模式键中允许的表达式
虽然从技术上完全可以将值约束和映射模式键中允许的表达式类型限制为仅属性查找和常量文字(就像PEP 634那样),但这样做没有任何明显的运行时价值,因此本 PEP 建议允许任何类型的基本表达式(基本表达式是语法中现有的节点类型,包括文字、名称、属性查找、函数调用、容器下标、带括号的组等),以及基本表达式上的高优先级一元运算符 (+
、-
、~
)。
虽然PEP 635确实多次强调字面量模式和值模式不是完整的表达式,但它从未阐明从该限制中获得的具体好处(只是一个理论上的吸引力,认为将静态检查与动态检查分开是有用的,即使编译器本身更宽松,代码风格工具也可以仍然执行此操作)。
我们上次实施此类限制是在装饰器表达式中,其主要结果是用户不得不忍受多年的笨拙的语法变通方法(例如,将任意表达式嵌套在仅返回其参数的函数调用中),以表达他们想要的行为,然后语言定义最终更新为允许任意表达式并让用户自己决定可读性。
在PEP 634中,与装饰器表达式的情况类似的是,在值模式中技术上支持任意表达式,它们只需要笨拙的变通方法,其中需要在 match 语句之前放置辅助类中指定所有要匹配的值。
# Allowing arbitrary match targets with PEP 634's value pattern syntax
class mt:
value = func()
match expr:
case (_, mt.value):
... # Handle the case where 'expr[1] == func()'
或者需要将其写成捕获模式和保护表达式的组合。
# Allowing arbitrary match targets with PEP 634's guard expressions
match expr:
case (_, _matched) if _matched == func():
... # Handle the case where 'expr[1] == func()'
本 PEP 建议跳过任何此类变通方法的要求,而是从一开始就支持任意值约束。
match expr:
case (__, == func()):
... # Handle the case where 'expr == func()'
是否实际编写这种代码是一个好主意将是风格指南和代码 linter 的主题,而不是语言编译器。
特别是,如果静态分析器无法遵循某些类型的动态检查,那么它们可以在分析时限制允许的表达式,而不是编译器在编译时限制它们。
还有一些表达式几乎肯定会导致毫无意义的结果(例如 yield
、yield from
、await
),因为模式缓存规则,约束表达式实际评估的次数将取决于实现。即使在这里,PEP 也持有一种观点,即如果用户确实想要,则允许他们编写无意义的内容。
除了最近更新的装饰器表达式之外,Python 的正式语法提供完全表达自由但在实践中几乎从未使用过的另一种情况是在 except
子句中:要匹配的异常几乎总是采用简单名称、带点名称或这些名称的元组的形式,但语言语法在这一点上允许任意表达式。这表明可以信任 Python 的用户群能够找到可读的方式来使用宽松的语言特性,通过避免编写难以阅读的结构,即使编译器允许它们。
这种宽松性带来了一个真正的具体实现方面的好处:编译器中数十行 match 语句特定代码被对用于编译表达式的现有代码的简单调用所取代(包括在 AST 验证传递、AST 优化传递、符号表分析传递和代码生成传递中)。这种实现优势不仅会惠及 CPython,还会惠及所有其他希望添加 match 语句支持的 Python 实现。
要求对映射模式键使用约束前缀标记
该提案的初始(未发表的)草案建议要求映射模式键为值约束,就像PEP 634要求它们是有效的字面量或值模式一样。
import constants
match config:
case {== "route": route}:
process_route(route)
case {== constants.DEFAULT_PORT: sub_config, **rest}:
process_config(sub_config, rest)
但是,额外的字符在语法上很嘈杂,并且与它在值约束中的用法不同(在其中它将它们与非模式表达式区分开来),该前缀在此处没有提供任何映射模式键中表达式的定位未已传达的其他信息。
因此,该提案已简化为从映射模式键中省略标记前缀。
此省略也与容器可能在其查找过程中包含同一性和相等性检查这一事实相一致——它们并不完全依赖于相等性检查,因为使用相等性约束前缀会错误地暗示这一点。
允许省略映射值约束的键/值分隔符
实例属性模式允许在编写属性值约束(如 case object{.attr == expr}
)时省略 :
分隔符。
考虑为映射值约束提供类似的简写,但允许它会允许彻底令人困惑的结构,例如 case {0 == 0}:
,其中编译器知道这是键 0
,其值约束为 == 0
,但人类读者会看到同义比较运算 0 == 0
。包含键/值分隔符后,对于人类读者来说,意图更加明显:case {0: == 0}:
参考实现
本 PEP 的参考实现草案[3]源自 Brandt Bucher 为PEP 634编写的参考实现[4]。
相对于本 PEP 的文本,参考实现草案尚未通过更普遍的检查 __match_args__
是否设置为 None
来补充 MATCH_CLASS
中几个内置类型和标准库类型的特殊情况处理。类定义的模式目前也仍然接受未定义 __match_args__
的类。
所有其他修改的模式都已更新为遵循本 PEP 而不是PEP 634。
match 模式反解析尚未迁移到更新的 v3 AST。
match 模式的 AST 验证器尚未实现。
AST 验证器总体上尚未经过审查,以确保它只在预期表达式节点的地方传递表达式节点。
本 PEP 中的示例尚未转换为测试用例,因此可能包含错别字和其他错误。
一些旧的 PEP 634 测试仍有待转换为新的 SyntaxError 测试。
文档尚未更新。
致谢
PEP 622 和 PEP 634/PEP 635/PEP 636 的作者认为,本 PEP 中的提案仅仅是尝试通过建议从更明确的语法开始,并可能在以后为特别常见的操作引入语法快捷方式,从而提高一个已经构建良好的想法的可读性,这比尝试*仅*定义快捷方式版本更好。对于这两个 PEP 相同(或至少非常相似)的规范区域,本 PEP 中描述预期行为的文本通常直接来自 PEP 634 文本。
Steven D’Aprano,他提出了一个令人信服的论点,即本 PEP 的关键目标可以通过使用现有的比较标记来实现,这些标记可以告诉编译器何时我们对“大多数用户大多数时候想要什么”的猜测不可避免地对至少一些用户在某些时候是错误的,并保留 PEP 634 的一些语法糖(具有略微不同的语义定义),以便在大多数情况下获得与 PEP 634 相同的简洁程度。(Paul Sokolosvsky 也独立地建议使用 ==
而不是 ?
作为等式约束更容易理解的前缀)。
Thomas Wouters,他发布了 PEP 640 并公开审查了结构化模式匹配提案,说服了本 PEP 的作者继续主张使用通配符模式语法,未来的 PEP 可以将其 plausibly 转换为一个硬关键字,该关键字始终跳过在任何期望简单名称的位置绑定引用,而不是无限期地作为此处提出的匹配模式特定的软关键字继续。
Joao Bueno 和 Jim Jewett,他们促使 PEP 作者更仔细地研究了在类模式和映射模式中提出的子元素捕获语法(特别是“向右捕获”的问题)。此次审查促使提案的 v2 和 v3 之间发生了重大变化。
参考文献
附录A – 完整语法
以下是 match_stmt
的完整修改语法,替换 PEP 634 中的附录 A。
超出标准 EBNF 的符号使用方式与 PEP 534 相同。
'KWD'
表示硬关键字。"KWD"
表示软关键字。SEP.RULE+
是RULE (SEP RULE)*
的简写。!RULE
是一个负向前瞻断言。
match_stmt: "match" subject_expr ':' NEWLINE INDENT case_block+ DEDENT
subject_expr:
| star_named_expression ',' [star_named_expressions]
| named_expression
case_block: "case" (guarded_pattern | open_pattern) ':' block
guarded_pattern: closed_pattern 'if' named_expression
open_pattern: # Pattern may use multiple tokens with no closing delimiter
| as_pattern
| or_pattern
as_pattern: [closed_pattern] pattern_as_clause
as_pattern_with_inferred_wildcard: pattern_as_clause
pattern_as_clause: 'as' pattern_capture_target
pattern_capture_target: !"__" NAME !('.' | '(' | '=')
or_pattern: '|'.simple_pattern+
simple_pattern: # Subnode where "as" and "or" patterns must be parenthesised
| closed_pattern
| value_constraint
value_constraint:
| eq_constraint
| id_constraint
eq_constraint: '==' closed_expr
id_constraint: 'is' closed_expr
closed_expr: # Require a single token or a closing delimiter in expression
| primary
| closed_factor
closed_factor: # "factor" is the main grammar node for these unary ops
| '+' primary
| '-' primary
| '~' primary
closed_pattern: # Require a single token or a closing delimiter in pattern
| wildcard_pattern
| group_pattern
| structural_constraint
wildcard_pattern: "__"
group_pattern: '(' open_pattern ')'
structural_constraint:
| sequence_constraint
| mapping_constraint
| attrs_constraint
| class_constraint
sequence_constraint: '[' [sequence_constraint_elements] ']'
sequence_constraint_elements: ','.sequence_constraint_element+ ','?
sequence_constraint_element:
| star_pattern
| simple_pattern
| as_pattern_with_inferred_wildcard
star_pattern: '*' (pattern_as_clause | wildcard_pattern)
mapping_constraint: '{' [mapping_constraint_elements] '}'
mapping_constraint_elements: ','.key_value_constraint+ ','?
key_value_constraint:
| closed_expr pattern_as_clause
| closed_expr ':' simple_pattern
| double_star_capture
double_star_capture: '**' pattern_as_clause
attrs_constraint:
| name_or_attr '{' [attrs_constraint_elements] '}'
name_or_attr: attr | NAME
attr: name_or_attr '.' NAME
attrs_constraint_elements: ','.attr_value_constraint+ ','?
attr_value_constraint:
| '.' NAME pattern_as_clause
| '.' NAME value_constraint
| '.' NAME ':' simple_pattern
| '.' NAME
class_constraint:
| name_or_attr '(' ')'
| name_or_attr '(' positional_patterns ','? ')'
| name_or_attr '(' class_constraint_attrs ')'
| name_or_attr '(' positional_patterns ',' class_constraint_attrs] ')'
positional_patterns: ','.positional_pattern+
positional_pattern:
| simple_pattern
| as_pattern_with_inferred_wildcard
class_constraint_attrs:
| '**' '{' [attrs_constraint_elements] '}'
附录B:抽象语法树更改摘要
本 PEP 向 AST 添加了以下新节点。
stmt = ...
| ...
| Match(expr subject, match_case* cases)
| ...
...
match_case = (pattern pattern, expr? guard, stmt* body)
pattern = MatchAlways
| MatchValue(matchop op, expr value)
| MatchSequence(pattern* patterns)
| MatchMapping(expr* keys, pattern* patterns)
| MatchAttrs(expr cls, identifier* attrs, pattern* patterns)
| MatchClass(expr cls, pattern* patterns, identifier* extra_attrs, pattern* extra_patterns)
| MatchRestOfSequence(identifier? target)
-- A NULL entry in the MatchMapping key list handles capturing extra mapping keys
| MatchAs(pattern? pattern, identifier target)
| MatchOr(pattern* patterns)
attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset)
matchop = EqCheck | IdCheck
附录C:相对于PEP 634的更改摘要
总体的 match
/case
语句语法和守卫表达式语法与 PEP 634 中相同。
相对于 PEP 634,本 PEP 做了以下关键更改。
- 在 AST 中定义了一种新的
pattern
类型,而不是为模式重用expr
类型。 - 新的
MatchAs
和MatchOr
AST 节点从expr
类型移动到pattern
类型。 - 通配符模式从
_
(单个下划线)更改为__
(双下划线),并在 AST 中获得一个专用的MatchAlways
节点。 - 由于意图不明确,值模式和字面量模式已被删除。
- 引入了一个新的表达式类别:“封闭表达式”。
- 封闭表达式要么是主表达式,要么是在高优先级一元运算符(
+
、-
、~
)之前的一个封闭表达式。 - 引入了一种新的模式类型:“值约束模式”。
- 值约束具有专用的
MatchValue
AST 节点,而不是允许Constant
(字面量)、UnaryOp
(负数)、BinOp
(复数)和Attribute
(属性查找)的组合。 - 值约束模式要么是等式约束,要么是恒等约束。
- 等式约束使用
==
作为其他任意封闭表达式的前面标记:== EXPR
。 - 恒等约束使用
is
作为其他任意封闭表达式的前面标记:is EXPR
。 - 由于意图不明确,捕获模式已被删除。所有捕获操作都使用
as
关键字(即使在序列匹配中),并在 AST 中表示为MatchAs
或MatchRestOfSequence
节点。 - 为了减少 AS 模式中的冗长,允许使用
as NAME
,其含义与__ as NAME
相同。 - 序列模式更改为*要求*使用方括号,而不是提供与赋值目标相同的语法灵活性(赋值语句允许通过使用任何用元组分隔的目标来指示可迭代解包,无论是否带有周围的括号或方括号)。
- 序列模式获得了一个专用的
MatchSequence
AST 节点,而不是重用List
。 - 映射模式更改为允许任意封闭表达式作为键。
- 映射模式获得了一个专用的
MatchMapping
AST 节点,而不是重用Dict
。 - 为了减少映射模式中的冗长,
KEY : __ as NAME
可以缩短为KEY as NAME
。 - 类模式不再使用单独的关键字参数语法进行属性匹配。相反,它们使用双星号语法,以及映射模式语法的变体,在属性名称前加上点前缀。
- 类模式获得了一个专用的
MatchClass
AST 节点,而不是重用Call
。 - 为了减少冗长,类属性匹配允许在要匹配的模式以
==
、is
或as
开头时省略:
。 - 类模式将任何将
__match_args__
设置为None
的类视为接受单个位置模式,该模式与整个对象匹配(避免了 PEP 634 中所需的特殊情况)。 - 当与未定义
__match_args__
的对象一起使用时,类模式会引发TypeError
。 - 添加了用于鸭子类型的专用语法,使得
case cls{...}:
基本上等价于case cls(**{...}):
,但跳过了检查__match_args__
是否存在。此模式还具有一个专用的 AST 节点,即MatchAttrs
。
请注意,推迟字面量模式也使得推迟是否需要在标记器中使用“INUMBER”标记来表示虚数字面量成为可能。如果没有它,解析器无法区分复数字面量与其他对常量的二元加法和减法运算,因此像 PEP 634 这样的提案必须在后续的编译步骤中执行工作以检查是否正确使用。
附录D:此提案更改的历史记录
此提案的首次发布版本主要遵循 PEP 634,但建议使用 ?EXPR
表示相等约束,使用 ?is EXPR
表示同一性约束,而不是 PEP 634 的值模式和字面量模式。
第二次发布的版本主要采用了 Steven D’Aprano 的一个反提案,该提案在许多情况下保留了 PEP 634 样式的推断约束,但也允许使用 == EXPR
表示显式相等约束,使用 is EXPR
表示显式同一性约束。
第三次发布(也是当前)版本完全放弃了推断模式,试图解决以下问题:模式 case {key: NAME}:
和 case cls(attr=NAME):
都会绑定 NAME
,尽管它出现在另一个子表达式的右侧,并且没有使用 as
关键字。修订后的提案还消除了编写 case TARGET1 as TARGET2:
的可能性,这将绑定到这两个给定的名称。在这些更改中,最令人担忧的是 case cls(attr=TARGET_NAME):
,因为它涉及在右侧使用绑定目标的 =
,这与赋值语句、函数调用和函数签名声明中发生的情况完全相反。
版权
本文件放置在公共领域或根据 CC0-1.0-Universal 许可证,以两者中许可范围更宽者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0642.rst
上次修改时间:2023-10-11 12:05:51 GMT