+ 和 * 和 +=
+ 和 * 都造新对象,不动操作数。听起来很安全,但有两个坑:
[[]] * 3造出的是三份同一个列表的引用,改一行三行都变。t[2] += [...]会改成功一半又抛异常。
陷阱一:* 造出的别名
Section titled “陷阱一:* 造出的别名”想初始化一个 3×3 的棋盘,直觉写法是错的:
>>> weird_board = [['_'] * 3] * 3>>> weird_board[1][2] = 'O'>>> weird_board[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]改成用推导式:
>>> board = [['_'] * 3 for i in range(3)]>>> board[1][2] = 'X'>>> board[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]两者为什么不同,看等价的 for 循环就清楚了:
| 写法 | 等价于 | 结果 |
|---|---|---|
[['_'] * 3] * 3 |
先造一行 row,再 append(row) 三次 |
三行是同一个对象 |
[['_'] * 3 for i in range(3)] |
每次迭代都 row = ['_'] * 3 造新的 |
三行互不相干 |
我在测试里直接断言了这个区别:
assert bad[0] is bad[1] # True —— 错误写法里三行是同一个对象陷阱二:+= 对元组做了什么
Section titled “陷阱二:+= 对元组做了什么”书里有一道题,先别看答案:
>>> t = (1, 2, [30, 40])>>> t[2] += [50, 60]t 会变成什么?
- A.
t变成(1, 2, [30, 40, 50, 60]) - B. 抛
TypeError - C. 都不对
- D. A 和 B 都对
正确答案是 D。我把实际结果跑出来了:
t = (1, 2, [30, 40])try: t[2] += [50, 60]except TypeError as e: print(type(e).__name__) # TypeErrorprint(t) # (1, 2, [30, 40, 50, 60])异常抛出来了,但那个 list 已经被改掉了。
看 s[a] += b 生成的字节码:
1 0 LOAD_NAME 0 (s) 2 LOAD_NAME 1 (a) 4 DUP_TOP_TWO 6 BINARY_SUBSCR 7 LOAD_NAME 2 (b) 9 INPLACE_ADD <-- 第一步:TOS += b,成功了 10 ROT_THREE 11 STORE_SUBSCR <-- 第二步:s[a] = TOS,失败分两步:
INPLACE_ADD—— 取出t[2]那个 list,对它做原地+=。成功,因为 list 可变。STORE_SUBSCR—— 把结果写回t[2]。失败,因为元组不支持元素赋值。
第一步的副作用不会因为第二步失败而回滚。这也说明了一件事: 增强赋值不是原子操作。
- 别往元组里放可变元素。
+=不是原子的,中途可能失败并留下痕迹。- 看字节码没那么难,
dis.dis()一眼就能看出这类问题。
+= 对可变和不可变序列的区别
Section titled “+= 对可变和不可变序列的区别”+= 靠 __iadd__ 实现。如果没有 __iadd__,Python 退回到 __add__:
a += ba有__iadd__(list、bytearray、array 都有)→ 原地改,id(a)不变。a没有__iadd__(tuple、str 没有)→ 等价于a = a + b,造新对象,id(a)变了。
实测:
>>> l = [1, 2, 3]>>> id(l)4311953800>>> l *= 2>>> id(l)4311953800 # 还是同一个对象
>>> t = (1, 2, 3)>>> id(t)4312681568>>> t *= 2>>> id(t)4301348296 # 换了新对象性能上的后果
Section titled “性能上的后果”不可变序列做重复拼接是很亏的:不能就地追加,只能把整个序列复制一遍再把新内容接上去。
所以 s = s + x 写在循环里,复杂度是 O(n²)。
test_augmented_assignment_puzzler_is_both—— 断言既抛异常又改了值,两个都成立test_board_wrong_aliases_all_rows—— 错误写法改一行三行都变test_boards_comparison_shows_aliasing—— 断言bad[0] is bad[1]test_iadd_on_mutable_is_in_place/test_iadd_on_immutable_creates_new_object—— 比较id是否变化