Python序列的+、*、+=、*=

序列是支持 + 、 * 、+=、*=操作的。


通常 + 号两侧的序列由相同类型的数据所构成,在拼接的过程中,两个被操作的序列都不会被修改,Python 会新建一个包含同样类型数据的序列来作为拼接的结果。

如果想要把一个序列复制几份然后再拼接起来,更快捷的做法是把这个序列乘以一个整数。同样,这个操作会产生一个新序列:

>>> l = [1, 2, 3]
>>> l * 5
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> 5 * 'abcd'
'abcdabcdabcdabcdabcd'

如果在 a * n 这个语句中,序列 a 里的元素是对其他可变对象的引用的话,你就需要格外注意了,因为这个式子的结果可能 会出乎意料。比如,你想用my_list = [[]] * 3 来初始化一个由列表组成的列表,但是你得到的列表里包含的 3 个元素其实是 3 个引用,而且这 3 个引用指向的都是同一个列表。这可能不是你想要的效果。

建立由列表组成的列表:

错误的示例:
使用 a * n

>>> weird_board = [['_'] * 3] * 3
>>> weird_board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> weird_board[1][2] = 'O'
>>> weird_board
[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]

外面的列表其实包含 3 个指向同一个列表的引用。当我们不做修改的时候,看起来都还好。 一旦我们试图标记第 1 行第 2 列的元素,就立马暴露了列表内的 3 个引用指向同一个对象的事实。

正确的示例:
使用列表推导初始化

>>> board = [['_'] * 3 for i in range(3)]
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[1][2] = 'X'
>>> board
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]

一直在说 + 和 *,但是别忘了我们还有 += 和 *=。随着目标序列的可变性的变化,这个两个运算符的结果也大相径庭。

+= 背后的特殊方法是 __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

对不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素。str 是一个例外,因为对字符串做 += 实在是太普遍了,所以 CPython 对它做了优化。为 str 初始化内存的时候,程序会为它留出额外的可扩展空间,因此进行增量操作的时候,并不会涉及复制原有字符串到新位置这类操作。(但id(str)内存地址是变了的)

一个关于+=的谜题:

>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]

到底会发生下面 4 种情况中的哪一种?

a. t 变成 (1, 2, [30, 40, 50, 60])。
b. 因为 tuple 不支持对它的元素赋值,所以会抛出 TypeError 异常。
c. 以上两个都不是。
d. a 和 b 都是对的。

我刚看到这个问题的时候,异常确定地选择了 b,但其实答案是 d,也 就是说 a 和 b 都是对的!

如果写成 t[2].extend([50, 60]) 就能避免这个异常。

s[a] += b 生成的字节码查看:

>>> import dis
>>> dis.dis('s[a] += b')
1 0 LOAD_NAME 0 (s)
3 LOAD_NAME 1 (a)
6 DUP_TOP_TWO
7 BINARY_SUBSCR # 1
8 LOAD_NAME 2 (b)
11 INPLACE_ADD # 2
12 ROT_THREE
13 STORE_SUBSCR # 3
14 LOAD_CONST 0 (None)
17 RETURN_VALUE

1、将 s[a] 的值存入 TOS(Top Of Stack,栈的顶端)。
2、计算 TOS += b。这一步能够完成,是因为 TOS 指向的是一个可变对象。
3、s[a] = TOS 赋值。这一步失败,是因为 s 是不可变的元组。

以上错误示例的教训:

  • 不要把可变对象放在元组里面。
  • 增量赋值不是一个原子操作。我们刚才也看到了,它虽然抛出了异常,但还是完成了操作。
  • 查看 Python 的字节码并不难,而且它对我们了解代码背后的运行机制很有帮助。

整理自《流畅的Python》第2章相关内容。