PEP 653 – 精确模式匹配语义
- 作者:
- Mark Shannon <mark at hotpy.org>
- 状态:
- 草案
- 类型:
- 标准跟踪
- 创建日期:
- 2021年2月9日
- 发布历史:
- 2021年2月18日
摘要
本PEP提出了一种模式匹配的语义,它尊重PEP 634的总体概念,但更加精确,更易于理解,并且应该更快。
除了PEP 634中的__match_args__属性之外,对象模型还将扩展两个特殊的(双下划线)属性,__match_container__和__match_class__,以支持模式匹配。这两个新属性都必须是整数,并且__match_args__必须是包含唯一字符串的元组。
通过本PEP
- 模式匹配的语义将更加清晰,因此模式更容易理解。
- 将能够以更高效的方式实现模式匹配。
- 通过允许类对它们匹配的模式有更多控制,模式匹配将更适用于复杂类。
动机
如PEP 634所述,Python中的模式匹配将被添加到Python 3.10中。不幸的是,PEP 634的语义不够精确,也不允许类充分控制它们如何匹配模式。
精确语义
PEP 634明确包含了一个关于未定义行为的部分。大量的未定义行为在C语言这样的语言中可能是可接受的,但在Python中应该尽量减少。Python中的模式匹配可以更精确地定义,而不会失去表达能力或性能。
增强类匹配的控制
PEP 634将类是序列还是映射的决定委托给collections.abc。并非所有可以被视为序列的类都注册为collections.abc.Sequence的子类。本PEP允许它们匹配序列模式,而无需完整的collections.abc.Sequence机制。
PEP 634赋予了一些内置类一种特殊的匹配形式,即“自身”匹配。例如,模式list(x)匹配一个列表并将该列表赋给x。通过允许类选择它们匹配的模式类型,其他类也可以使用这种形式。
例如,使用sympy,我们可能希望编写
# a*a == a**2
case Mul(args=[Symbol(a), Symbol(b)]) if a == b:
return Pow(a, 2)
这要求sympy类Symbol进行“自身”匹配。对于sympy来说,使用PEP 634支持这种模式是可能的,但有点棘手。有了本PEP,它可以很容易地实现[1]。
健壮性
有了这个PEP,模式匹配期间对属性的访问变得明确且确定。这使得在匹配具有隐藏副作用的对象(例如对象关系映射器)时,模式匹配不易出错。对象将对其自身的解构拥有更多控制权,这有助于防止属性访问产生副作用时出现意外结果。
PEP 634在确定一个值可以匹配哪些模式时依赖于collections.abc模块,如果需要会隐式导入它。本PEP将消除这些导入带来的令人惊讶的导入错误和误导性审计事件。
高效实现
本PEP中提出的语义将允许高效实现,部分原因是其精确的语义,部分原因是使用了对象模型。
通过精确的语义,可以推断出哪些代码转换是正确的,从而有效地应用优化。
因为对象模型是Python的核心部分,实现已经高效地处理了特殊属性查找。查找特殊属性比在抽象基类上执行子类测试快得多。
基本原理
对象模型和特殊方法是Python语言的核心。因此,实现对其支持良好。使用特殊属性进行模式匹配,使得模式匹配能够以与实现的其他部分良好集成的方式实现,从而更容易维护,并且可能表现更好。
一个匹配语句执行一系列模式匹配。通常,匹配一个模式有三个部分:
- 该值能否匹配这种模式?
- 当解构时,该值是否匹配这个特定模式?
- 守卫是否为真?
为了确定一个值能否匹配特定类型的模式,我们添加了__match_container__和__match_class__属性。这允许以高效的方式确定值的类型。
规范
对象模型的补充
__match_container__ 和 __match_class__ 属性将被添加到 object 中。__match_container__ 应该由希望匹配映射或序列模式的类重写。__match_class__ 应该由希望更改匹配类模式时默认行为的类重写。
__match_container__ 必须是一个整数,且应严格为以下值之一
0
MATCH_SEQUENCE = 1
MATCH_MAPPING = 2
MATCH_SEQUENCE 用于指示该类的实例可以匹配序列模式。
MATCH_MAPPING 用于指示该类的实例可以匹配映射模式。
__match_class__ 必须是一个整数,且应严格为以下值之一
0
MATCH_SELF = 8
MATCH_SELF 用于指示,对于单个位置参数类模式,将使用主题而不进行解构。
注意
在本文档的其余部分,我们将仅通过名称来指代上述值。Python和C都将提供符号常量,并且这些值将永远不会改变。
object 的特殊属性值如下
__match_container__ = 0
__match_class__= 0
__match_args__ = ()
这些特殊属性将正常继承。
如果__match_args__被重写,则它必须包含一个由唯一字符串组成的元组。它可以为空。
注意
__match_args__将按照PEP 634的规定,为数据类和命名元组自动生成。
模式匹配实现**不**要求检查任何这些属性是否按指定行为。如果__match_container__、__match_class__或__match_args__的值未按指定,则实现可能引发任何异常,或匹配错误的模式。当然,如果实现能够高效地进行检查,则可以自由检查这些属性并提供有意义的错误消息。
匹配过程的语义
在下文中,所有形式为 $var 的变量都是临时变量,对 Python 程序不可见。它们可能通过内省可见,但这属于实现细节,不应依赖。伪语句 FAIL 用于表示该模式匹配失败,应继续匹配下一个模式。如果控制流在未遇到 FAIL 的情况下到达翻译结束,则表示已匹配成功,后续模式将被忽略。
形式为 $ALL_CAPS 的变量是持有语法元素的元变量,它们不是普通变量。因此,$VARS = $items 并不是将 $items 赋值给 $VARS,而是将 $items 解包到 $VARS 所持有的变量中。例如,对于抽象语法 case [$VARS]: 和具体语法 case[a, b]:,那么 $VARS 将持有变量 (a, b),而不是这些变量的值。
伪函数QUOTE接受一个变量并返回该变量的名称。例如,如果元变量$VAR持有变量foo,那么QUOTE($VAR) == "foo"。
下面列出的所有不在原始源中存在的额外代码都不会触发行事件,符合PEP 626。
前言
在匹配任何模式之前,对被匹配的表达式进行评估
match expr:
转换为
$value = expr
捕获模式
捕获模式总是匹配,因此是不可驳的匹配
case capture_var:
转换为
capture_var = $value
通配符模式
通配符模式总是匹配,所以
case _:
转换为
# No code -- Automatically matches
字面量模式
字面量模式
case LITERAL:
转换为
if $value != LITERAL:
FAIL
除非字面量是 None、True 或 False 之一,否则它将转换为
if $value is not LITERAL:
FAIL
值模式
值模式
case value.pattern:
转换为
if $value != value.pattern:
FAIL
序列模式
不包含星号模式的模式
case [$VARS]:
转换为
$kind = type($value).__match_container__
if $kind != MATCH_SEQUENCE:
FAIL
if len($value) != len($VARS):
FAIL
$VARS = $value
示例:[2]
包含星号模式的模式
case [$VARS]
转换为
$kind = type($value).__match_container__
if $kind != MATCH_SEQUENCE:
FAIL
if len($value) < len($VARS):
FAIL
$VARS = $value # Note that $VARS includes a star expression.
示例:[3]
映射模式
不包含双星号模式的模式
case {$KEYWORD_PATTERNS}:
转换为
$sentinel = object()
$kind = type($value).__match_container__
if $kind != MATCH_MAPPING:
FAIL
# $KEYWORD_PATTERNS is a meta-variable mapping names to variables.
for $KEYWORD in $KEYWORD_PATTERNS:
$tmp = $value.get(QUOTE($KEYWORD), $sentinel)
if $tmp is $sentinel:
FAIL
$KEYWORD_PATTERNS[$KEYWORD] = $tmp
示例:[4]
包含双星号模式的模式
case {$KEYWORD_PATTERNS, **$DOUBLE_STARRED_PATTERN}:
转换为
$kind = type($value).__match_container__
if $kind != MATCH_MAPPING:
FAIL
# $KEYWORD_PATTERNS is a meta-variable mapping names to variables.
$tmp = dict($value)
if not $tmp.keys() >= $KEYWORD_PATTERNS.keys():
FAIL:
for $KEYWORD in $KEYWORD_PATTERNS:
$KEYWORD_PATTERNS[$KEYWORD] = $tmp.pop(QUOTE($KEYWORD))
$DOUBLE_STARRED_PATTERN = $tmp
示例:[5]
类模式
没有参数的类模式
case ClsName():
转换为
if not isinstance($value, ClsName):
FAIL
带单个位置模式的类模式
case ClsName($VAR):
转换为
$kind = type($value).__match_class__
if $kind == MATCH_SELF:
if not isinstance($value, ClsName):
FAIL
$VAR = $value
else:
As other positional-only class pattern
仅位置类模式
case ClsName($VARS):
转换为
if not isinstance($value, ClsName):
FAIL
$attrs = ClsName.__match_args__
if len($attr) < len($VARS):
raise TypeError(...)
try:
for i, $VAR in enumerate($VARS):
$VAR = getattr($value, $attrs[i])
except AttributeError:
FAIL
示例:[6]
所有关键字模式的类模式
case ClsName($KEYWORD_PATTERNS):
转换为
if not isinstance($value, ClsName):
FAIL
try:
for $KEYWORD in $KEYWORD_PATTERNS:
$tmp = getattr($value, QUOTE($KEYWORD))
$KEYWORD_PATTERNS[$KEYWORD] = $tmp
except AttributeError:
FAIL
示例:[7]
带位置和关键字模式的类模式
case ClsName($VARS, $KEYWORD_PATTERNS):
转换为
if not isinstance($value, ClsName):
FAIL
$attrs = ClsName.__match_args__
if len($attr) < len($VARS):
raise TypeError(...)
$pos_attrs = $attrs[:len($VARS)]
try:
for i, $VAR in enumerate($VARS):
$VAR = getattr($value, $attrs[i])
for $KEYWORD in $KEYWORD_PATTERNS:
$name = QUOTE($KEYWORD)
if $name in pos_attrs:
raise TypeError(...)
$KEYWORD_PATTERNS[$KEYWORD] = getattr($value, $name)
except AttributeError:
FAIL
示例:[8]
嵌套模式
上述规范假设模式不嵌套。对于嵌套模式,通过引入临时捕获模式递归地应用上述转换。
例如,模式
case [int(), str()]:
转换为
$kind = type($value).__match_class__
if $kind != MATCH_SEQUENCE:
FAIL
if len($value) != 2:
FAIL
$value_0, $value_1 = $value
#Now match on temporary values
if not isinstance($value_0, int):
FAIL
if not isinstance($value_1, str):
FAIL
守卫
守卫在翻译的其余部分之后转换为测试
case pattern if guard:
转换为
[translation for pattern]
if not guard:
FAIL
不符合规范的特殊属性
所有类都应确保 __match_container__、__match_class__ 和 __match_args__ 的值符合规范。因此,实现可以假定(无需检查)以下内容为真
__match_container__ == 0 or __match_container__ == MATCH_SEQUENCE or __match_container__ == MATCH_MAPPING
__match_class__ == 0 or __match_class__ == MATCH_SELF
并且 __match_args__ 是一个由唯一字符串组成的元组。
标准库中类的特殊属性值
对于核心内置容器类,__match_container__ 将是
list:MATCH_SEQUENCEtuple:MATCH_SEQUENCEdict:MATCH_MAPPING字节数组: 0字节: 0str: 0
命名元组的 __match_container__ 将设置为 MATCH_SEQUENCE。
- 所有其他标准库类,如果
issubclass(cls, collections.abc.Mapping)为真,则其__match_container__将设置为MATCH_MAPPING。 - 所有其他标准库类,如果
issubclass(cls, collections.abc.Sequence)为真,则其__match_container__将设置为MATCH_SEQUENCE。
对于以下内置类,__match_class__ 将设置为 MATCH_SELF
布尔值字节数组字节浮点数不可变集合int集合str列表元组字典
合法优化
上述语义意味着实现中存在大量的冗余工作和复制。然而,通过对朴素实现采用语义保留转换,可以高效地实现上述语义。
在执行匹配时,允许实现将以下函数和方法视为纯函数
对于任何支持MATCH_SEQUENCE的类
* ``cls.__len__()``
* ``cls.__getitem__()``
对于任何支持MATCH_MAPPING的类
* ``cls.get()`` (Two argument form only)
实现被允许做出以下假设
isinstance(obj, cls)可以自由地替换为issubclass(type(obj), cls),反之亦然。isinstance(obj, cls)对于任何(obj, cls)对都将始终返回相同的结果,因此可以省略重复调用。- 读取
__match_container__、__match_class__或__match_args__中的任何一个都是纯操作,并且可以缓存。 - 序列(即
__match_container__ == MATCH_SEQUENCE不为零的任何类)不会因迭代、下标或调用len()而修改。因此,在应用于不可变序列时,这些操作可以在它们等效的地方自由互换。 - 映射,即任何
__match_container__ == MATCH_MAPPING不为零的类,将不会捕获get()方法的第二个参数。因此,$sentinel值可以自由重用。
事实上,鼓励实现者做出这些假设,因为这很可能显著提高性能。
安全隐患
无。
实施
从规范得出的朴素实现效率不高。幸运的是,有一些相当直接的转换可以用来提高性能。性能应该与PEP 634的实现(在撰写本文时)在3.10版本发布时相当。进一步的性能改进可能要等到3.11版本发布。
可能进行的优化
以下内容不属于规范,而是帮助开发人员创建高效实现的指导原则。
将评估拆分为多个“通道”
由于匹配每个模式的第一步是检查其类型,因此可以将所有类型检查组合成一个多路分支,放在匹配的开头。然后可以将案例列表复制到多个“通道”中,每个通道对应一种类型。这样就可以很容易地从每个通道中移除不匹配的案例。根据类型,每个通道可以采用不同的优化策略。请注意,匹配子句的主体不需要复制,只需复制模式即可。
序列模式
这可能是最复杂且性能提升最大的优化。由于每个模式只能匹配一个长度范围,通常只有一个长度,因此测试序列可以重写为对序列的显式迭代,只尝试匹配适用于该序列长度的模式。
例如
case []:
A
case [x]:
B
case [x, y]:
C
case other:
D
大致可以编译为
# Choose lane
$i = iter($value)
for $0 in $i:
break
else:
A
goto done
for $1 in $i:
break
else:
x = $0
B
goto done
for $2 in $i:
del $0, $1, $2
break
else:
x = $0
y = $1
C
goto done
other = $value
D
done:
映射模式
这里最好的策略可能是根据映射的大小和存在的键形成一个决策树。没有必要重复测试键的存在。例如
match obj:
case {a:x, b:y}:
W
case {a:x, c:y}:
X
case {a:x, b:_, c:y}:
Y
case other:
Z
如果检查情况 X 时键 "a" 不存在,则无需再次检查 Y。
映射通道大致可以这样实现
# Choose lane
if len($value) == 2:
if "a" in $value:
if "b" in $value:
x = $value["a"]
y = $value["b"]
goto W
if "c" in $value:
x = $value["a"]
y = $value["c"]
goto X
elif len($value) == 3:
if "a" in $value and "b" in $value:
x = $value["a"]
y = $value["c"]
goto Y
other = $value
goto Z
本PEP与PEP 634的差异摘要
语义上的变化可以概括为:
- 要求
__match_args__是一个字符串的**元组**,而不仅仅是一个序列。这使得模式匹配更加健壮和可优化,因为__match_args__可以被假定为不可变的。 - 选择可以匹配的容器模式类型使用
cls.__match_container__而不是issubclass(cls, collections.abc.Mapping)和issubclass(cls, collections.abc.Sequence)。 - 如果需要,允许类通过设置
__match_class__ = 0来完全退出解构。 - 匹配模式时的行为定义得更精确,但除此之外没有改变。
语法没有变化。PEP 636教程中给出的所有示例都应继续按现有方式工作。
被拒绝的想法
使用实例字典中的属性
本PEP的早期版本仅在使用__match_class__为默认值匹配类模式时,使用实例字典中的属性。目的是避免捕获绑定方法和其他合成属性。然而,这也意味着属性被忽略了。
对于这个类
class C:
def __init__(self):
self.a = "a"
@property
def p(self):
...
def m(self):
...
理想情况下,我们会匹配属性“a”和“p”,但不匹配“m”。然而,没有通用的方法可以做到这一点,因此本PEP现在遵循PEP 634的语义。
在主体上查找 __match_args__,而不是模式上
本PEP的早期版本在主体类而非模式中指定的类上查找 __match_args__。此方案因以下几个原因被拒绝:
* Using the class specified in the pattern is more amenable to optimization and can offer better performance.
* Using the class specified in the pattern has the potential to provide better error reporting is some cases.
* Neither approach is perfect, both have odd corner cases. Keeping the status quo minimizes disruption.
将 __match_class__ 和 __match_container__ 合并为一个值
本PEP的早期版本将 __match_class__ 和 __match_container__ 合并为一个值 __match_kind__。使用单一值在性能方面略有优势,但很可能在重写类匹配行为时导致容器匹配发生意外更改,反之亦然。
延迟的想法
本PEP的最初版本包含了匹配类型MATCH_POSITIONAL和特殊方法__deconstruct__,这将允许类完全控制其匹配。这对于像sympy这样的库非常重要。
例如,使用sympy,我们可能希望编写
# sin(x)**2 + cos(x)**2 == 1
case Add(Pow(sin(a), 2), Pow(cos(b), 2)) if a == b:
return 1
对于sympy来说,用当前的模式匹配支持位置模式是可能的,但很棘手。有了这些额外的功能,它可以很容易地实现[9]。
这个想法将在3.11的未来PEP中提出。然而,对于3.10开发周期来说,进行这样的更改为时已晚。
拥有一个独立的值来拒绝所有类匹配
在本PEP的早期版本中,__match_class__有一个不同的值,允许类不匹配任何需要解构的类模式。然而,一旦引入MATCH_POSITIONAL,这将变得冗余,并且使极少数情况的规范复杂化。
代码示例
class Symbol:
__match_class__ = MATCH_SELF
这个
case [a, b] if a is b:
转换为
$kind = type($value).__match_container__
if $kind != MATCH_SEQUENCE:
FAIL
if len($value) != 2:
FAIL
a, b = $value
if not a is b:
FAIL
这个
case [a, *b, c]:
转换为
$kind = type($value).__match_container__
if $kind != MATCH_SEQUENCE:
FAIL
if len($value) < 2:
FAIL
a, *b, c = $value
这个
case {"x": x, "y": y} if x > 2:
转换为
$kind = type($value).__match_container__
if $kind != MATCH_MAPPING:
FAIL
$tmp = $value.get("x", $sentinel)
if $tmp is $sentinel:
FAIL
x = $tmp
$tmp = $value.get("y", $sentinel)
if $tmp is $sentinel:
FAIL
y = $tmp
if not x > 2:
FAIL
这个
case {"x": x, "y": y, **z}:
转换为
$kind = type($value).__match_container__
if $kind != MATCH_MAPPING:
FAIL
$tmp = dict($value)
if not $tmp.keys() >= {"x", "y"}:
FAIL
x = $tmp.pop("x")
y = $tmp.pop("y")
z = $tmp
这个
match ClsName(x, y):
转换为
if not isinstance($value, ClsName):
FAIL
$attrs = ClsName.__match_args__
if len($attr) < 2:
FAIL
try:
x = getattr($value, $attrs[0])
y = getattr($value, $attrs[1])
except AttributeError:
FAIL
这个
match ClsName(a=x, b=y):
转换为
if not isinstance($value, ClsName):
FAIL
try:
x = $value.a
y = $value.b
except AttributeError:
FAIL
这个
match ClsName(x, a=y):
转换为
if not isinstance($value, ClsName):
FAIL
$attrs = ClsName.__match_args__
if len($attr) < 1:
raise TypeError(...)
$positional_names = $attrs[:1]
try:
x = getattr($value, $attrs[0])
if "a" in $positional_names:
raise TypeError(...)
y = $value.a
except AttributeError:
FAIL
class Basic:
__match_class__ = MATCH_POSITIONAL
def __deconstruct__(self):
return self._args
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0653.rst