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_SEQUENCE
tuple
:MATCH_SEQUENCE
dict
:MATCH_MAPPING
bytearray
: 0bytes
: 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
bool
bytearray
bytes
float
frozenset
int
set
str
list
tuple
dict
合法优化
上述语义意味着实现中存在大量冗余的工作和复制。但是,可以通过对朴素实现进行语义保持转换来有效地实现上述语义。
在执行匹配时,实现允许将以下函数和方法视为纯函数
对于任何支持 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
值。
实际上,鼓励实现做出这些假设,因为这可能会导致显着提高性能。
安全影响
无。
实现
从规范得出的朴素实现效率不会很高。幸运的是,有一些相当简单的转换可用于提高性能。在 3.10 发布时,性能应该与 PEP 634(撰写本文时)的实现相当。进一步的性能改进可能需要等到 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