我们把讨论集中在增量加法(+=)上,但是这些概念对 *= 和其他增量运算符来说都是一样的。
+= 背后的特殊方法是 __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__ 这个方法。 上面所说的这些关于 += 的概念也适用于 *=,不同的是,后者相对应的 是 __imul__。
>>> l = [1, 2, 3] >>> id(l) 4311953800>>> l *= 2 >>> l [1, 2, 3, 1, 2, 3] >>> id(l) 4311953800>>> t = (1, 2, 3) >>> id(t) 4312681568>>> t *= 2 >>> id(t) 4301348296
对不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一个 新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素。只有str 是一个例外,因为对字符串做 += 实在是太普遍了,所以 CPython 对它做了优化。为 str 初始化内存的时候,程序会为它留出额外的可扩展空间,因此进行增量操作的时候,并不会涉及复制原有字符串到新位置这类操作。
一个谜题:
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
结果会发生什么呢?没人料到的结果:t[2] 被改动了,但是也有异常抛出。
以下两张截图,分别代表示例中 t 的初始和最终状态:
初始:
最终:
首先将 s[a] 的值存入 TOS(Top Of Stack,栈的顶端),然后计算 TOS += b。这俩步能够完成,是因为 TOS 指向的是一个可变对象(也就是上图中的列表)。但s[a] = TOS 赋值这一步失败,是因为 s 是不可变的元组。
至此我得到了 2 个教训:
1.不要把可变对象放在元组里面。
2.增量赋值不是一个原子操作。它虽然抛出了异常,但还是完成了操作。