PEP 203 – 增强赋值
- 作者:
- Thomas Wouters <thomas at python.org>
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2000年7月13日
- Python 版本:
- 2.0
- 历史记录:
- 2000年8月14日
引言
本 PEP 描述了 Python 2.0 的增强赋值提案。本 PEP 跟踪此功能的状态和所有权,该功能计划在 Python 2.0 中引入。它包含对该功能的描述,并概述了支持该功能所需的更改。本 PEP 总结了在邮件列表论坛中进行的讨论[1],并在适当的地方提供了进一步信息的 URL。此文件的 CVS 修订历史记录包含最终的历史记录。
提议的语义
添加增强赋值到 Python 的提议补丁引入了以下新的运算符
+= -= *= /= %= **= <<= >>= &= ^= |=
它们实现了与普通二元形式相同的运算符,只是当左侧对象支持时,运算就地执行,并且左侧仅计算一次。
它们确实表现为增强赋值,因为除了它们要执行的二元运算之外,它们还执行所有正常的加载和存储操作。因此,给定表达式
x += y
对象x
被加载,然后y
加到它上面,然后将结果对象存储回原始位置。对这两个参数执行的确切操作取决于x
的类型,以及可能y
的类型。
Python 中增强赋值背后的想法不仅仅是编写将二元运算结果存储在其左侧操作数中的常见做法的一种更简单的方法,而且还是一种方法,让左侧操作数知道它应该自身进行操作,而不是创建其自身的修改副本。
为了使这成为可能,向 Python 类和 C 扩展类型添加了许多新的钩子,当相关对象用作增强赋值操作的左侧时,就会调用这些钩子。如果类或类型没有实现就地钩子,则使用特定二元运算的普通钩子。
因此,给定一个实例对象x
,表达式
x += y
尝试调用x.__iadd__(y)
,这是__add__
的就地变体。如果不存在__iadd__
,则尝试x.__add__(y)
,最后如果__add__
也缺少,则尝试y.__radd__(x)
。没有__iadd__
的右侧变体,因为这需要y
知道如何就地修改x
,这至少是不安全的。__iadd__
钩子应该类似于__add__
,返回操作的结果(可能是self
),该结果将分配给变量x
。
对于 C 扩展类型,钩子是PyNumberMethods
和PySequenceMethods
结构的成员。应用一些特殊语义来使这些方法的使用以及 Python 实例对象和 C 类型的混合尽可能地不令人意外。
在x <augop> y
(或使用PyNumber_InPlace
API 函数的类似情况)的一般情况下,正在操作的主要对象是x
。这与普通的二元运算不同,在普通的二元运算中,x
和y
可以被认为是协作的,因为与二元运算不同,就地运算中的操作数不能交换。但是,当不支持就地修改时,就地运算确实回退到普通的二元运算,从而产生以下规则
- 如果左侧对象(
x
)是实例对象,并且它具有__coerce__
方法,则使用y
作为参数调用该函数。如果强制转换成功,并且生成的左侧对象与x
不同,则停止将其作为就地处理,并使用强制转换后的x
和y
作为参数调用普通二元运算的相应函数。操作的结果就是该函数返回的内容。如果强制转换没有为
x
产生不同的对象,或者x
没有定义__coerce__
方法,并且x
具有此操作的相应__ihook__
,则使用y
作为参数调用该方法,并且操作的结果就是该方法返回的内容。 - 否则,如果左侧对象不是实例对象,但其类型确实为此操作定义了就地函数,则使用
x
和y
作为参数调用该函数,并且操作的结果就是该函数返回的内容。请注意,在这种情况下,对
x
或y
都不进行强制转换,并且 C 类型接收实例对象作为第二个参数是完全有效的;这在普通的二元运算中是不可能发生的。 - 否则,将其完全像普通的二元运算(非就地)一样处理,包括参数强制转换。简而言之,如果任一参数是实例对象,则通过
__coerce__
、__hook__
和__rhook__
解析操作。否则,两个对象都是 C 类型,它们会被强制转换并传递给相应的函数。 - 如果找不到处理操作的方法,则引发
TypeError
,并显示特定于操作的错误消息。 - 存在一些特殊情况来解释
+
和*
的情况,它们对序列具有特殊含义:对于+
(序列连接),如果 C 类型定义了sq_concat
或sq_inplace_concat
,则根本不进行任何强制转换。对于*
(序列重复),在调用sq_inplace_repeat
和sq_repeat
之前,y
会被转换为 C 整数。即使y
是实例,也会这样做,但如果x
是实例,则不会这样做。
就地函数应始终返回一个新引用,如果操作确实就地执行,则返回到旧的x
对象,或者返回到新对象。
基本原理
向 Python 添加此功能有两个主要原因:表达式的简单性和对就地操作的支持。最终结果是在语法简单性和表达式简单性之间进行权衡;与大多数新功能一样,增强赋值并没有添加以前不可能实现的任何东西。它仅仅使这些事情更容易做到。
添加增强赋值将使 Python 的语法更加复杂。赋值操作不再只有一个,而是有 12 个,其中 11 个还执行二元运算。但是,这 11 种新的赋值形式很容易理解,因为赋值与二元运算之间的耦合,并且它们不需要进行大的概念飞跃就能理解。此外,确实有增强赋值的语言表明它们是一个流行且使用广泛的功能。表单的表达式
<x> = <x> <operator> <y>
在这些语言中足够常见,以至于额外的语法值得拥有,并且 Python 并没有明显更少的此类表达式。事实上,恰恰相反,因为在 Python 中,您还可以使用二元运算符连接列表,这是非常频繁的操作。将上述表达式写成
<x> <operator>= <y>
既更易读又更不容易出错,因为读者可以立即清楚地知道正在更改的是<x>
,而不是<x>
被替换为几乎与<x>
完全不同但并非完全不同的东西。
新的就地操作对于矩阵计算和其他需要大型对象的应用程序特别有用。为了有效地处理可用的程序内存,此类包不能盲目地使用当前的二元运算。因为这些运算总是创建新对象,所以向现有(大型)对象添加单个项目将导致复制整个对象(这可能导致应用程序内存不足),添加单个项目,然后可能删除原始对象,具体取决于引用计数。
为了解决此问题,这些包目前必须使用方法或函数就地修改对象,这肯定不如增强赋值表达式易读。增强赋值不会解决这些包的所有问题,因为某些操作无法用有限的二元运算符集来表达,但这是一个开始。PEP 211 正在考虑添加新的运算符。
新方法
提议的实现添加了以下 11 个可能的钩子,Python 类可以实现这些钩子来重载增强赋值操作
__iadd__
__isub__
__imul__
__idiv__
__imod__
__ipow__
__ilshift__
__irshift__
__iand__
__ixor__
__ior__
__iadd__
中的i代表就地。
对于 C 扩展类型,添加了以下结构体成员。
到 PyNumberMethods
binaryfunc nb_inplace_add;
binaryfunc nb_inplace_subtract;
binaryfunc nb_inplace_multiply;
binaryfunc nb_inplace_divide;
binaryfunc nb_inplace_remainder;
binaryfunc nb_inplace_power;
binaryfunc nb_inplace_lshift;
binaryfunc nb_inplace_rshift;
binaryfunc nb_inplace_and;
binaryfunc nb_inplace_xor;
binaryfunc nb_inplace_or;
到 PySequenceMethods
binaryfunc sq_inplace_concat;
intargfunc sq_inplace_repeat;
为了保持二进制兼容性,tp_flags
TypeObject 成员用于确定相关 TypeObject 是否已为这些槽分配了空间。在进行二进制兼容性的彻底更改之前(这可能在 2.0 之前发生,也可能不发生),想要使用新结构体成员的代码必须首先使用 PyType_HasFeature()
宏检查它们是否可用。
if (PyType_HasFeature(x->ob_type, Py_TPFLAGS_HAVE_INPLACE_OPS) &&
x->ob_type->tp_as_number && x->ob_type->tp_as_number->nb_inplace_add) {
/* ... */
即使在测试方法槽是否为 NULL
值之前,也必须进行此检查!该宏仅测试槽是否可用,而不是它们是否填充了方法。
实现
增强赋值的当前实现 [2] 除了已经涵盖的方法和槽之外,还添加了 13 个新的字节码和 13 个新的 API 函数。
API 函数只是当前二元运算 API 函数的原地版本。
PyNumber_InPlaceAdd(PyObject *o1, PyObject *o2);
PyNumber_InPlaceSubtract(PyObject *o1, PyObject *o2);
PyNumber_InPlaceMultiply(PyObject *o1, PyObject *o2);
PyNumber_InPlaceDivide(PyObject *o1, PyObject *o2);
PyNumber_InPlaceRemainder(PyObject *o1, PyObject *o2);
PyNumber_InPlacePower(PyObject *o1, PyObject *o2);
PyNumber_InPlaceLshift(PyObject *o1, PyObject *o2);
PyNumber_InPlaceRshift(PyObject *o1, PyObject *o2);
PyNumber_InPlaceAnd(PyObject *o1, PyObject *o2);
PyNumber_InPlaceXor(PyObject *o1, PyObject *o2);
PyNumber_InPlaceOr(PyObject *o1, PyObject *o2);
PySequence_InPlaceConcat(PyObject *o1, PyObject *o2);
PySequence_InPlaceRepeat(PyObject *o, int count);
它们要么调用 Python 类钩子(如果任一对象是 Python 类实例),要么调用 C 类型的数字或序列方法。
新的字节码是
INPLACE_ADD
INPLACE_SUBTRACT
INPLACE_MULTIPLY
INPLACE_DIVIDE
INPLACE_REMAINDER
INPLACE_POWER
INPLACE_LEFTSHIFT
INPLACE_RIGHTSHIFT
INPLACE_AND
INPLACE_XOR
INPLACE_OR
ROT_FOUR
DUP_TOPX
INPLACE_*
字节码镜像 BINARY_*
字节码,除了它们被实现为对 InPlace
API 函数的调用。其他两个字节码是实用字节码:ROT_FOUR
的行为类似于 ROT_THREE
,除了旋转最上面的四个堆栈项。
DUP_TOPX
是一个字节码,它接受一个单一参数,该参数应该是一个介于 1 和 5(含)之间的整数,表示要作为一个块复制的项数。给定一个像这样的堆栈(其中列表的右侧是堆栈的顶部)
[1, 2, 3, 4, 5]
DUP_TOPX 3
将复制最上面的 3 个项,从而产生以下堆栈
[1, 2, 3, 4, 5, 3, 4, 5]
参数为 1 的 DUP_TOPX
与 DUP_TOP
相同。5 的限制纯粹是实现限制。增强赋值的实现只需要参数为 2 和 3 的 DUP_TOPX
,并且可以通过一些 DUP_TOP
和 ROT_*
来避免使用这个新的操作码。
未解决的问题
PyNumber_InPlace
API 只是普通 PyNumber
API 的一个子集:只包含支持增强赋值语法所需的那些函数。如果需要其他原地 API 函数,可以稍后添加。
DUP_TOPX
字节码是一个便利字节码,实际上并不是必需的。应该考虑这个字节码是否值得拥有。目前似乎没有其他可能的用途。
版权
本文档已置于公共领域。
参考文献
来源:https://github.com/python/peps/blob/main/peps/pep-0203.rst
上次修改时间:2023-09-09 17:39:29 GMT