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
另一个动机是,类型通常没有自然排序,但仍需要进行相等性比较。目前,这样的类型**必须**实现比较并因此定义一个任意排序,仅仅是为了测试相等性。
此外,对于某些对象类型,相等性测试可以比排序测试更有效地实现;例如,长度不同的列表和字典是不等的,但排序需要检查一些(可能全部)项目。
前期工作
富比较以前曾被提出;特别是David Ascher在Numerical Python的经验之后提出。
它也作为附录包含在下文。本PEP中的大部分内容都来源于David的提案。
关注点
- 向后兼容性,包括Python级别(使用
__cmp__的类无需更改)和C级别(定义tp_comparea的扩展无需更改,使用PyObject_Compare()的代码即使比较的对象使用新的富比较方案也必须能工作)。 - 当 A
- 如果一个类重写了 x==y 但没有重写其他,x!=y 应该计算为 not(x==y) 还是失败? < 和 >= 之间,或 > 和 <= 之间类似的关系如何?
- 同样,我们是否应该允许 x
x 计算? x<=y 从 not(x>y) 计算? x==y 从 y==x 计算,或 x!=y 从 y!=x 计算? - 当比较运算符返回元素级比较时,对于 AA and C
, A or C这样的快捷运算符,应该怎么做?- 对于
min()和max(),'in' 和 'not in' 运算符,list.sort(),字典键比较,以及内置操作的其他比较用途,应该怎么做?
提议的解决方案
- 完全向后兼容性可以通过以下方式实现。当一个对象定义
tp_compare()但不定义tp_richcompare(),并且请求富比较时,tp_compare()的结果以显而易见的方式使用。例如,如果请求“<”,如果tp_compare()引发异常,则抛出异常;如果tp_compare()为负,则结果为1;如果为零或正,则结果为0。依此类推。完全向前兼容性可以通过以下方式实现。当在一个实现了
tp_richcompare()的对象上请求经典比较时,最多使用三次比较:首先尝试 ==,如果它返回真,则返回0;接下来,尝试 <,如果它返回真,则返回-1;接下来,尝试 >,如果它返回真,则返回+1。如果任何尝试的运算符返回非布尔值(见下文),则布尔转换引发的异常将直接传递。如果所有尝试的运算符都没有返回真,则接下来尝试经典比较的备用方案。(我仔细考虑了尝试这三种比较的顺序。曾几何时,我有一个令人信服的论据,基于循环数据结构的比较行为,认为应该按此顺序进行。但由于该代码再次发生变化,我现在不确定它是否仍然有区别。)
- 任何返回布尔值集合而不是单个布尔值的类型都应该定义
nb_nonzero()以引发异常。此类类型被视为非布尔值。 - == 和 != 运算符不被认为是彼此的补集(例如,IEEE 754 浮点数不满足此条件)。是否实现取决于类型。 < 和 >=,或 > 和 <= 之间也类似;有许多例子表明这些假设不成立(例如 tabnanny)。
- Python **确实**假设反射规则。因此,解释器可能会将 y>x 与 x
=x 与 x<=y 交换,并可能交换 x==y 和 x!=y 的参数。(注意:Python 目前假设 x==x 始终为真,x!=x 永远不为真;这不应该被假设。) - 在当前的提案中,当 A
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 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 以外的值(或引发异常)。该机制完全向后兼容,并且可以在 C PyObject 类型或 Python 类定义的级别进行控制。拟议的机制有三个相互协作的部分
- 使用类型对象结构中的最后一个槽位存储指向富比较函数的指针
- 为类添加特殊方法
- 为内置的
cmp()函数添加一个可选参数。
动机
Python 对象的当前比较协议假设任何两个 Python 对象都可以比较(从 Python 1.5 开始,对象比较可以引发异常),并且任何比较的返回值都应该是 -1、0 或 1。-1 表示比较函数的第一个参数小于第二个参数,+1 表示相反,0 表示两个对象相等。虽然这种机制允许建立排序关系(例如用于列表对象的 sort() 方法),但它在 Numeric 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__ 特殊方法通过 richcmp 函数的第三个参数的另一个值来实现,用于C级类型。此方法将执行数组的布尔比较(目前在 umath 模块中作为 logical_and ufunc 实现)。
因此,富比较返回的对象应该总是测试为真,但应该定义另一个特殊方法,该方法创建它们及其参数的布尔组合。
这个解决方案的优点是允许链式比较适用于数组,但缺点是它要求比较数组始终返回真(在一个理想的世界中,我会让它们在真值测试时总是引发异常,因为测试“if a>b:”的含义非常模糊。
已经存在的处理整数比较的内联仍然适用,因此在最常见的情况下不会产生性能成本。
来源:https://github.com/python/peps/blob/main/peps/pep-0207.rst