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

另一个动机是,类型通常没有自然排序,但仍需要进行相等性比较。目前,这样的类型**必须**实现比较并因此定义一个任意排序,仅仅是为了测试相等性。

此外,对于某些对象类型,相等性测试可以比排序测试更有效地实现;例如,长度不同的列表和字典是不等的,但排序需要检查一些(可能全部)项目。

前期工作

富比较以前曾被提出;特别是David Ascher在Numerical Python的经验之后提出。

它也作为附录包含在下文。本PEP中的大部分内容都来源于David的提案。

关注点

  1. 向后兼容性,包括Python级别(使用 __cmp__ 的类无需更改)和C级别(定义 tp_comparea 的扩展无需更改,使用 PyObject_Compare() 的代码即使比较的对象使用新的富比较方案也必须能工作)。
  2. 当 A
  3. 如果一个类重写了 x==y 但没有重写其他,x!=y 应该计算为 not(x==y) 还是失败? < 和 >= 之间,或 > 和 <= 之间类似的关​​系如何?
  4. 同样,我们是否应该允许 xx 计算? x<=y 从 not(x>y) 计算? x==y 从 y==x 计算,或 x!=y 从 y!=x 计算?
  5. 当比较运算符返回元素级比较时,对于 AA and C, A or C 这样的快捷运算符,应该怎么做?
  6. 对于 min()max(),'in' 和 'not in' 运算符, list.sort(),字典键比较,以及内置操作的其他比较用途,应该怎么做?

提议的解决方案

  1. 完全向后兼容性可以通过以下方式实现。当一个对象定义 tp_compare() 但不定义 tp_richcompare(),并且请求富比较时,tp_compare() 的结果以显而易见的方式使用。例如,如果请求“<”,如果 tp_compare() 引发异常,则抛出异常;如果 tp_compare() 为负,则结果为1;如果为零或正,则结果为0。依此类推。

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

    (我仔细考虑了尝试这三种比较的顺序。曾几何时,我有一个令人信服的论据,基于循环数据结构的比较行为,认为应该按此顺序进行。但由于该代码再次发生变化,我现在不确定它是否仍然有区别。)

  2. 任何返回布尔值集合而不是单个布尔值的类型都应该定义 nb_nonzero() 以引发异常。此类类型被视为非布尔值。
  3. == 和 != 运算符不被认为是彼此的补集(例如,IEEE 754 浮点数不满足此条件)。是否实现取决于类型。 < 和 >=,或 > 和 <= 之间也类似;有许多例子表明这些假设不成立(例如 tabnanny)。
  4. Python **确实**假设反射规则。因此,解释器可能会将 y>x 与 x=x 与 x<=y 交换,并可能交换 x==y 和 x!=y 的参数。(注意:Python 目前假设 x==x 始终为真,x!=x 永远不为真;这不应该被假设。)
  5. 在当前的提案中,当 A
  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 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() 调用。

提议的机制

  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__ 特殊方法通过 richcmp 函数的第三个参数的另一个值来实现,用于C级类型。此方法将执行数组的布尔比较(目前在 umath 模块中作为 logical_and ufunc 实现)。

因此,富比较返回的对象应该总是测试为真,但应该定义另一个特殊方法,该方法创建它们及其参数的布尔组合。

这个解决方案的优点是允许链式比较适用于数组,但缺点是它要求比较数组始终返回真(在一个理想的世界中,我会让它们在真值测试时总是引发异常,因为测试“if a>b:”的含义非常模糊。

已经存在的处理整数比较的内联仍然适用,因此在最常见的情况下不会产生性能成本。


来源:https://github.com/python/peps/blob/main/peps/pep-0207.rst

最后修改:2025-02-01 08:55:40 GMT