Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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 的提案。

关注点

  1. 向后兼容性,在 Python 层面(使用__cmp__的类不需要更改)和 C 层面(定义tp_comparea的扩展不需要更改,使用PyObject_Compare()的代码即使被比较的对象使用了新的丰富比较方案也必须正常工作)。
  2. 当 A<B 返回一个元素级比较的矩阵时,一个容易犯的错误是将此表达式用在布尔上下文中。如果没有特殊的预防措施,它将始终为真。此用法应改为引发异常。
  3. 如果一个类重写了 x==y 但没有重写其他运算符,那么 x!=y 应该计算为 not(x==y),还是失败?< 和 >= 之间,或者 > 和 <= 之间是否存在类似的关系?
  4. 类似地,我们是否应该允许 x<y 从 y>x 计算?以及 x<=y 从 not(x>y) 计算?以及 x==y 从 y==x 计算,或者 x!=y 从 y!=x 计算?
  5. 当比较运算符返回元素级比较时,如何处理 A<B<C、A<B and C<DA<B or C<D 这样的快捷运算符?
  6. 如何处理min()max()、'in' 和 'not in' 运算符、list.sort()、字典键比较以及内置操作对比较的其他用法?

拟议的解决方案

  1. 可以按如下方式实现完全向后兼容性。当一个对象定义了tp_compare()但没有定义tp_richcompare(),并且请求丰富比较时,将以显而易见的方式使用tp_compare()的结果。例如,如果请求“<”,则如果tp_compare()引发异常则引发异常,如果tp_compare()为负则结果为 1,如果为零或正则结果为 0。等等。

    可以按如下方式实现完全向前兼容性。当对实现了tp_richcompare()的对象请求经典比较时,最多使用三个比较:首先尝试 ==,如果它返回 true,则返回 0;接下来,尝试 <,如果它返回 true,则返回 -1;接下来,尝试 >,如果它返回 true,则返回 +1。如果任何尝试的运算符返回非布尔值(见下文),则将由转换为布尔值引发的异常传递出去。如果所有尝试的运算符都没有返回 true,则接下来尝试经典比较回退。

    (我认真思考了应该按什么顺序尝试这三个比较。有一段时间,我有一个令人信服的论据来按此顺序进行,基于循环数据结构比较的行为。但由于该代码再次发生了变化,我不确定它是否还有区别。)

  2. 任何返回布尔值集合而不是单个布尔值的类型都应定义nb_nonzero()以引发异常。这种类型被认为是非布尔值。
  3. == 和 != 运算符不被假定为彼此的补码(例如,IEEE 754 浮点数不满足此条件)。如果需要,类型本身负责实现这一点。< 和 >=,或者 > 和 <= 也是如此;有很多例子表明这些假设不成立(例如 tabnanny)。
  4. Python **假定**自反规则。因此,解释器可能会交换 y>x 和 x<y,y>=x 和 x<=y,并可能交换 x==y 和 x!=y 的参数。(注意:Python 目前假设 x==x 始终为真,x!=x 从不为真;不应该假设这一点。)
  5. 在当前提案中,当 A<B 返回一个元素级比较的数组时,此结果被认为是非布尔值,并且快捷运算符将其解释为布尔值会引发异常。David Ascher 的提案试图解决这个问题;我认为代码生成器中的额外复杂性不值得这样做。您可以编写 (A<B)&(B<C),而不是 A<B<C。
  6. 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() 调用。

拟议机制

  1. 对类型对象 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 等名称)。

  2. 添加类的特殊方法

    希望支持富比较机制的类必须添加以下一个或多个新的特殊方法

    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 实现级别而不是用户定义的方法级别执行操作码分派。

  3. 向内置函数 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