跳转到内容

+ 和 * 和 +=

+ 和 * 都造新对象,不动操作数。听起来很安全,但有两个坑:

  1. [[]] * 3 造出的是三份同一个列表的引用,改一行三行都变。
  2. t[2] += [...] 会改成功一半又抛异常。

想初始化一个 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 —— 错误写法里三行是同一个对象

书里有一道题,先别看答案:

>>> 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__) # TypeError
print(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,失败

分两步:

  1. INPLACE_ADD —— 取出 t[2] 那个 list,对它做原地 +=。成功,因为 list 可变。
  2. STORE_SUBSCR —— 把结果写回 t[2]。失败,因为元组不支持元素赋值。

第一步的副作用不会因为第二步失败而回滚。这也说明了一件事: 增强赋值不是原子操作。

  1. 别往元组里放可变元素。
  2. += 不是原子的,中途可能失败并留下痕迹。
  3. 看字节码没那么难,dis.dis() 一眼就能看出这类问题。

+= 靠 __iadd__ 实现。如果没有 __iadd__,Python 退回到 __add__:

a += b
  • a 有 __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 # 换了新对象

不可变序列做重复拼接是很亏的:不能就地追加,只能把整个序列复制一遍再把新内容接上去。 所以 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 是否变化