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的语法更加复杂。现在,除了一个赋值操作之外,还有十二个赋值操作,其中十一个还执行二元操作。然而,这十一种新形式的赋值很容易理解为赋值和二元操作之间的耦合,并且它们不需要大的概念飞跃来理解。此外,拥有增强赋值的语言表明,它们是一个流行且广泛使用的功能。以下形式的表达式
<x> = <x> <operator> <y>
在这些语言中非常普遍,使得额外的语法变得值得,而Python中这些表达式的数量并没有显著减少。事实上,恰恰相反,因为在Python中你还可以用二元运算符连接列表,这是非常频繁的操作。将上述表达式写成
<x> <operator>= <y>
既更具可读性,又更不易出错,因为它让读者立即明白是<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