序列增量赋值详解

增量赋值运算符+=*=的表现取决于它们的第一个操作对象。简单起见,我们把讨论集中在增量加法上,但是这些概念对*=和其他增量运算符来说都是一样的。

+=背后的特殊方法是__iadd__(用于“就地加法”)。但是如果一个类没有实现这个方法的话,Python 会退一步调用__add__。考虑下面这个简单的表达式:

>>> a += b

如果 a 实现了__iadd__方法,就会调用这个方法。同时对可变序列(例如 list、bytearray 和 array.array)来说,a 会就地改动,就像调用了a.extend(b)一样。但是如果 a 没有实现__iadd__的话,a += b这个表达式的效果就变得跟a = a + b一样了:首先计算a + b,得到一个新的对象,然后赋值给 a。也就是说,在这个表达式中,变量名会不会被关联到新的对象,完全取决于这个类型有没有实现__iadd__这个方法。

总体来讲,可变序列一般都实现了__iadd__方法,因此+=是就地加法。而不可变序列根本就不支持这个操作,对这个方法的实现也就无从谈起。

上面所说的这些关于+=的概念也适用于*=,不同的是,后者对应的是__imul__

接下来有个小例子,展示的是*=在可变和不可变序列上的作用。

>>> l = [1, 2, 3]
>>> id(l)
1801317143624        # 刚开始时列表的ID
>>> l *= 2
>>> l
[1, 2, 3, 1, 2, 3]
>>> id(l)
1801317143624        # 运用增量乘法后,列表的ID没变,新元素追加到列表上
>>> t = (1, 2, 3)
>>> id(t)
1801320562184        # 元组最开始的ID
>>> t *= 2
>>> id(t)
1801319212712        # 运用增量乘法后,新的元组被创建

对不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素。

str 是一个例外,因为对字符串做+=实在是太普遍了,所以 CPython 对它做了优化。为 str 初始化内存的时候,程序会为它留出额外的可扩展空间,因此进行增量操作的时候,只要增加的量不超过这个额外空间,就不会涉及复制原有字符串到新位置这类操作。