PEP 207 – 丰富比较
- 作者:
- Guido van Rossum <guido at python.org>,David Ascher <DavidA at ActiveState.com>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建:
- 2000年7月25日
- Python 版本:
- 2.1
- 历史记录:
摘要
本 PEP 提出了一些用于比较的新特性
- 允许分别重载 <、>、<=、>=、==、!=,在类和 C 扩展中均可。
- 允许任何这些重载运算符返回布尔值以外的结果。
动机
主要动机来自 NumPy,其用户一致认为 A<B 应该返回一个元素级比较结果的数组;他们目前必须将其写成 less(A,B),因为 A<B 只能返回布尔值或引发异常。
另一个动机是,在很多情况下,类型没有自然的顺序,但仍然需要进行相等性比较。目前,这种类型**必须**实现比较并因此定义一个任意的顺序,以便能够进行相等性测试。
此外,对于某些对象类型,相等性测试的实现效率可能比顺序测试高得多;例如,长度不同的列表和字典是不相等的,但顺序需要检查某些(可能全部)项。
先前工作
之前也有人提出过丰富比较;特别是 David Ascher,在使用数值 Python 之后提出的。
它也包含在下面的附录中。本 PEP 中的大部分内容都源自 David 的提案。
关注点
- 向后兼容性,在 Python 层面(使用
__cmp__
的类不需要更改)和 C 层面(定义tp_comparea
的扩展不需要更改,使用PyObject_Compare()
的代码即使被比较的对象使用了新的丰富比较方案也必须正常工作)。 - 当 A<B 返回一个元素级比较的矩阵时,一个容易犯的错误是将此表达式用在布尔上下文中。如果没有特殊的预防措施,它将始终为真。此用法应改为引发异常。
- 如果一个类重写了 x==y 但没有重写其他运算符,那么 x!=y 应该计算为 not(x==y),还是失败?< 和 >= 之间,或者 > 和 <= 之间是否存在类似的关系?
- 类似地,我们是否应该允许 x<y 从 y>x 计算?以及 x<=y 从 not(x>y) 计算?以及 x==y 从 y==x 计算,或者 x!=y 从 y!=x 计算?
- 当比较运算符返回元素级比较时,如何处理 A<B<C、
A<B and C<D
、A<B or C<D
这样的快捷运算符? - 如何处理
min()
和max()
、'in' 和 'not in' 运算符、list.sort()
、字典键比较以及内置操作对比较的其他用法?
拟议的解决方案
- 可以按如下方式实现完全向后兼容性。当一个对象定义了
tp_compare()
但没有定义tp_richcompare()
,并且请求丰富比较时,将以显而易见的方式使用tp_compare()
的结果。例如,如果请求“<”,则如果tp_compare()
引发异常则引发异常,如果tp_compare()
为负则结果为 1,如果为零或正则结果为 0。等等。可以按如下方式实现完全向前兼容性。当对实现了
tp_richcompare()
的对象请求经典比较时,最多使用三个比较:首先尝试 ==,如果它返回 true,则返回 0;接下来,尝试 <,如果它返回 true,则返回 -1;接下来,尝试 >,如果它返回 true,则返回 +1。如果任何尝试的运算符返回非布尔值(见下文),则将由转换为布尔值引发的异常传递出去。如果所有尝试的运算符都没有返回 true,则接下来尝试经典比较回退。(我认真思考了应该按什么顺序尝试这三个比较。有一段时间,我有一个令人信服的论据来按此顺序进行,基于循环数据结构比较的行为。但由于该代码再次发生了变化,我不确定它是否还有区别。)
- 任何返回布尔值集合而不是单个布尔值的类型都应定义
nb_nonzero()
以引发异常。这种类型被认为是非布尔值。 - == 和 != 运算符不被假定为彼此的补码(例如,IEEE 754 浮点数不满足此条件)。如果需要,类型本身负责实现这一点。< 和 >=,或者 > 和 <= 也是如此;有很多例子表明这些假设不成立(例如 tabnanny)。
- Python **假定**自反规则。因此,解释器可能会交换 y>x 和 x<y,y>=x 和 x<=y,并可能交换 x==y 和 x!=y 的参数。(注意:Python 目前假设 x==x 始终为真,x!=x 从不为真;不应该假设这一点。)
- 在当前提案中,当 A<B 返回一个元素级比较的数组时,此结果被认为是非布尔值,并且快捷运算符将其解释为布尔值会引发异常。David Ascher 的提案试图解决这个问题;我认为代码生成器中的额外复杂性不值得这样做。您可以编写 (A<B)&(B<C),而不是 A<B<C。
min()
和list.sort()
操作只会使用 < 运算符;max() 只会使用 > 运算符。'in' 和 'not in' 运算符以及字典查找只会使用 == 运算符。
实现提案
这紧密遵循 David Ascher 的提案。
C API
- 新函数
PyObject *PyObject_RichCompare(PyObject *, PyObject *, int)
这执行请求的丰富比较,返回一个 Python 对象或引发异常。第三个参数必须是 Py_LT、Py_LE、Py_EQ、Py_NE、Py_GT 或 Py_GE 之一。
int PyObject_RichCompareBool(PyObject *, PyObject *, int)
这执行请求的丰富比较,返回一个布尔值:-1 表示异常,0 表示假,1 表示真。第三个参数必须是 Py_LT、Py_LE、Py_EQ、Py_NE、Py_GT 或 Py_GE 之一。请注意,当
PyObject_RichCompare()
返回一个非布尔值对象时,PyObject_RichCompareBool()
将引发异常。 - 新 typedef
typedef PyObject *(*richcmpfunc) (PyObject *, PyObject *, int);
- 类型对象中的新槽,替换备用 tp_xxx7
richcmpfunc tp_richcompare;
这应该是一个与
PyObject_RichCompare()
具有相同签名的函数,并执行相同的比较。至少一个参数是正在使用其 tp_richcompare 槽的类型的类型,但另一个参数可能具有不同的类型。如果该函数无法比较对象的特定组合,则应返回一个对Py_NotImplemented
的新引用。 PyObject_Compare()
被更改为尝试丰富比较(但前提是经典比较没有定义)。
对解释器的更改
- 无论何时调用
PyObject_Compare()
是为了获取特定比较的结果(例如,在list.sort()
中,当然还有 ceval.c 中的比较运算符),代码都会更改为改为调用PyObject_RichCompare()
或PyObject_RichCompareBool()
;如果 C 代码需要知道比较的结果,则对结果调用PyObject_IsTrue()
(这可能会引发异常)。 - 大多数当前定义了比较的内置类型将被修改为改为定义丰富比较。(这是可选的;到目前为止,我已经转换了列表、元组、复数和数组,并且不确定是否会转换其他类型。)
类
- 类可以定义新的特殊方法
__lt__
、__le__
、__eq__
、__ne__
、__gt__
、__ge__
来重载相应的运算符。(即,<、<=、==、!=、>、>=。你必须喜欢 Fortran 的传统。)如果一个类也定义了__cmp__
,则只有在尝试了__lt__
等并返回NotImplemented
时才会使用它。
版权
本文档已置于公共领域。
附录
以下是 David Ascher 的原始提案的大部分内容(版本 0.2.1,日期为 1998 年 7 月 22 日星期三 16:49:28;我已经省略了目录、历史记录和补丁部分)。它解决了上面几乎所有问题。
摘要
提出了一种新的机制,允许 Python 对象的比较返回-1、0 或 1 以外的值(或引发异常)。此机制完全向后兼容,并且可以在 CPyObject
类型或 Python 类定义级别进行控制。拟议机制包含三个相互协作的部分
- 使用类型对象结构中的最后一个槽来存储指向丰富比较函数的指针
- 为类添加特殊方法
- 向内置
cmp()
函数添加可选参数。
动机
Python 对象的当前比较协议假定任何两个 Python 对象都可以进行比较(从 Python 1.5 开始,对象比较可以引发异常),并且任何比较的返回值都应该是-1、0 或 1。-1 表示比较函数的第一个参数小于第二个参数,+1 表示反之,0 表示这两个对象相等。虽然此机制允许建立顺序关系(例如,供列表对象的sort()
方法使用),但在数值 Python (NumPy) 的上下文中,它已被证明是有限的。
具体来说,NumPy 允许创建支持大多数数值运算符的多维数组。因此
x = array((1,2,3,4)) y = array((2,2,4,4))
是两个 NumPy 数组。虽然它们可以进行逐元素相加,
z = x + y # z == array((3,4,7,8))
但不能在当前框架中进行比较 - 发布版本的 NumPy 会比较指针(因此会产生垃圾信息),这是在最近添加了在比较函数中引发异常的能力(在 1.5 版中)之前唯一的解决方案。
即使能够引发异常,当前协议也使数组比较变得毫无用处。为了解决这个问题,NumPy 包含了一些执行比较的函数:less()
、less_equal()
、greater()
、greater_equal()
、equal()
、not_equal()
。这些函数返回与它们的输入参数(模广播)形状相同的数组,并根据每个元素对的比较结果填充 0 和 1。因此,例如,使用上面定义的数组 x 和 y
less(x,y)
将是一个包含数字 (1,0,0,0) 的数组。
当前的建议是修改 Python 对象接口,以允许 NumPy 包使 x < y 返回与 less(x,y) 相同的结果。确切的返回值由 NumPy 包决定 - 该提案真正要求的是更改 Python 核心,以便扩展对象能够返回 -1、0、1 以外的其他内容,如果它们的作者选择这样做的话。
现状
当前协议是在 C 级别,每个对象类型都定义了一个 tp_compare
槽,它是一个指向函数的指针,该函数接受两个 PyObject*
引用并返回 -1、0 或 1。此函数由 C API 中定义的 PyObject_Compare()
函数调用。 PyObject_Compare()
也会被接受两个参数的内置函数 cmp()
调用。
拟议机制
- 对类型对象 C 结构的更改
迄今为止为将来扩展保留的
PyTypeObject
中的最后一个可用槽用于可选地存储指向新比较函数的指针,该函数的类型为 richcmpfunc,由typedef PyObject *(*richcmpfunc) Py_PROTO((PyObject *, PyObject *, int));
定义。此函数接受三个参数。前两个是要比较的对象,第三个是对应于操作码的整数(LT、LE、EQ、NE、GT、GE 中的一个)。如果此槽保留为 NULL,则不支持该对象类型的富比较(除了其类提供下面描述的特殊方法的类实例)。
上述操作码需要添加到已发布的 Python/C API 中(可能以 Py_LT、Py_LE 等名称)。
- 添加类的特殊方法
希望支持富比较机制的类必须添加以下一个或多个新的特殊方法
def __lt__(self, other): ... def __le__(self, other): ... def __gt__(self, other): ... def __ge__(self, other): ... def __eq__(self, other): ... def __ne__(self, other): ...
当类实例位于相应运算符(<、<=、>、>=、== 和 != 或 <>)的左侧时,将调用每个方法。参数 other 设置为运算符右侧的对象。这些方法的返回值由类实现者决定(毕竟,这就是该提案的全部意义)。
如果运算符左侧的对象未定义适当的富比较运算符(无论是在 C 级别还是使用特殊方法之一),则比较将被反转,并且右侧运算符将使用相反的运算符被调用,并且两个对象将被交换。假设 a < b 和 b > a 是等价的,a <= b 和 b >= a 也是等价的,并且 == 和 != 是可交换的(例如,a == b 当且仅当 b == a)。
例如,如果 obj1 是支持富比较协议的对象,而 x 和 y 是不支持富比较协议的对象,则 obj1 < x 将调用 obj1 的
__lt__
方法,并将 x 作为第二个参数。x < obj1 将调用 obj1 的__gt__
方法,并将 x 作为第二个参数,而 x < y 将只使用现有的(非富)比较机制。上述机制使得类可以避免实现
__lt__
和__le__
或__gt__
和__ge__
。可以在比较机制中添加更多智能功能,但选择此有限的允许“交换”集是因为它不需要基础架构对返回值进行任何处理(否定)。选择六种特殊方法而不是单个(例如__richcmp__
)方法是为了允许在 C 实现级别而不是用户定义的方法级别执行操作码分派。 - 向内置函数
cmp()
添加可选参数内置函数
cmp()
仍用于简单的比较。对于富比较,它会调用第三个参数,其中一个是“<”、“<=”、“>”、“>=”、“==”、“!=”、“<>” (最后两个具有相同的含义)。当使用这些字符串之一作为第三个参数调用时,cmp()
可以返回任何 Python 对象。否则,它只能像以前一样返回 -1、0 或 1。
链式比较
问题
最好允许比较返回 -1、0 或 1 以外内容的对象用于链式比较,例如
x < y < z
目前,Python 将其解释为
temp1 = x < y
if temp1:
return y < z
else:
return temp1
请注意,这需要测试比较结果的真值,并可能“短路”右侧比较测试。换句话说,比较结果的真值决定了链式操作的结果。这在数组的情况下存在问题,因为如果 x、y 和 z 是三个数组,则用户期望
x < y < z
是一个由 0 和 1 组成的数组,其中 1 位于对应于 y 中介于 x 和 z 中对应元素之间的元素的位置。换句话说,必须评估右侧,而不管 x < y 的结果如何,这与解析器当前使用的机制不兼容。
解决方案
Guido 提到,一种可能的解决方法是更改链式比较生成的代码,以允许对数组进行智能链式比较。以下是他的想法和我的建议的混合。为 x < y < z 生成的代码将等效于
temp1 = x < y
if temp1:
temp2 = y < z
return boolean_combine(temp1, temp2)
else:
return temp1
其中 boolean_combine 是一个新的函数,其执行的操作类似于以下内容
def boolean_combine(a, b):
if hasattr(a, '__boolean_and__') or \
hasattr(b, '__boolean_and__'):
try:
return a.__boolean_and__(b)
except:
return b.__boolean_and__(a)
else: # standard behavior
if a:
return b
else:
return 0
其中 __boolean_and__
特殊方法由富比较函数的第三个参数的另一个值在 C 级别类型中实现。此方法将执行数组的布尔比较(目前在 umath 模块中作为 logical_and ufunc 实现)。
因此,富比较返回的对象应始终测试为真,但应定义另一个特殊方法来创建它们及其参数的布尔组合。
此解决方案的优点是可以使链式比较对数组起作用,但缺点是它要求比较数组始终返回真(在理想情况下,我希望它们在真值测试时始终引发异常,因为测试“if a>b:”的含义非常模糊)。
已经存在的处理整数比较的内联仍然适用,从而在最常见的情况下不会产生性能损失。
来源:https://github.com/python/peps/blob/main/peps/pep-0207.rst
上次修改时间:2023-09-09 17:39:29 GMT