增量赋值运算符
+=
和*=
的表现取决于它们的第一个操作对象,是可变类型还是不可变类型。+=
背后的特殊方法是__iadd__
(用于“就地加法”)。但是如果操作对象没有实现这个方法的话,Python 会退一步调用__add__
。
对可变序列使用增量赋值运算符:
如果 a
实现了 __iadd__
方法,就会调用这个方法。对可变序列(例如 list
、bytearray
和 array.array
)来说,a
会就地改动,就像调用了 a.extend(b)
一样。
a = [1, 2, 3]
b = [4, 5, 6]
print(id(a)) # 4566048648
a += b
print(a) # [1, 2, 3, 4, 5, 6]
print(id(a)) # 4566048648 id 没有发生变化,说明仍然是原来的列表
对不可变序列使用增量赋值运算符:
但是如果 a
没有实现 __iadd__
的话(不可变序列没有实现此方法),a += b
这个表达式的效果就变得跟 a = a + b
一样了:首先计算 a + b
,得到一个新的对象,然后赋值给 a
。
a = (1, 2, 3)
b = (4, 5, 6)
print(id(a)) # 4308234672
a += b
print(a) # (1, 2, 3, 4, 5, 6)
print(id(a)) # 4307974760 id 发生变化,说明是新建对象
对不可变序列进行重复拼接操作的话,效率会很低,因为每次都会创建一个新的对象,解释器需要把原对象中的元素复制到新对象中,然后再添加新的元素。
总体来讲,可变序列一般都实现了 __iadd__
方法,因此 +=
是就地加法;而不可变序列根本就不支持这个操作,没有实现这个方法。
上面所说的这些关于 +=
的概念也适用于 *=
,不同的是,后者相对应的是 __imul__
。
特殊的字符串类型:
字符串( str )类型也是不可变序列,但是因为字符串拼接操作太常见了,所以 CPython 对字符串拼接操作做了优化,为 str 初始化内存空间时,程序会为它留出额外的可扩展空间,因此在进行增量操作的时候,并不会涉及复制原有字符串到新位置的操作。
一个关于+=的谜题:
在终端中,下面代码中的打印结果是什么?
A. t 变成 (1, 2, [30, 40, 50, 60])。
B. 因为 tuple 不支持对它的元素赋值,所以会抛出 TypeError 异常。
C. 以上两个都不是。
D. a 和 b 都是对的。
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
>>>
>>> print(t)
答案是:D,原因如下:
Python 在整个过程中干了什么?
➊ 将 t[2] 的值存入 TOS(Top Of Stack,栈的顶端)。
➋ 计算 TOS += [50, 60]。这一步能够完成,是因为 TOS 指向的是一个可变对象(列表)。
➌ t[2] = TOS 赋值。这一步失败,是因为 s 是不可变的元组,抛出了错误 TypeError: 'tuple' object does not support item assignment;
❹ 再次打印 t 。输出结果:(1, 2, [30, 40, 50, 60]) ;
总结:
1. 不要把可变对象放在元组里;
2. 增量赋值不是一个原子操作。我们刚才也看到了,它虽然抛出了异常,但还是完成了操作。