序列增量赋值详解
增量赋值运算符+=
和*=
的表现取决于它们的第一个操作对象。简单起见,我们把讨论集中在增量加法上,但是这些概念对*=
和其他增量运算符来说都是一样的。
+=
背后的特殊方法是__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 初始化内存的时候,程序会为它留出额外的可扩展空间,因此进行增量操作的时候,只要增加的量不超过这个额外空间,就不会涉及复制原有字符串到新位置这类操作。
这个必须点赞👍