相信很多人对 tuple 和 list 的区别的理解是 tuple 是一个不可变的序列,不能对它的元素赋值。我之前也是这么理解的,举个例子:

In : a = (1, 2, 3)
In : a[3] = 4
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-5-d840230b1ac3> in <module>()
----> 1 a[3] = 4

TypeError: 'tuple' object does not support item assignment

In : a
Out: (1, 2, 3)

也就是一个元组生成,它的元素就不再能改变了。

但是相信很多人见过下面这样的玩法(有人把它当做 Python 的一个笑话):

In : a = (1, 2, [3, 4])
In : a[2] += [5, 6]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-2-84fb4a701b92> in <module>()
----> 1 a[2] += [5, 6]

TypeError: 'tuple' object does not support item assignment

In : a
Out: (1, 2, [3, 4, 5, 6])

明确的报错了,可是为了 a 的值还是改了呢?

我曾经思考过这个问题,直接上感觉是「对列表[3, 4] 的赋值成功,但是后来发生的元组赋值失败造成的」,但是一直苦于没有证据。直到昨晚看《Fluent Python》的时候,才从作者哪里获得了肯定的答案。今天我们用 dis 模块来分析 += 所产生的 bytecode (把 python 代码反汇编为字节码指令):

In : import dis
In : a = (1, 2, [3, 4])
In : dis.dis('a[2] += [5, 6]')
  1           0 LOAD_NAME                0 (a)
              2 LOAD_CONST               0 (2)
              4 DUP_TOP_TWO
              6 BINARY_SUBSCR
              8 LOAD_CONST               1 (5)
             10 LOAD_CONST               2 (6)
             12 BUILD_LIST               2
             14 INPLACE_ADD
             16 ROT_THREE
             18 STORE_SUBSCR
             20 LOAD_CONST               3 (None)
             22 RETURN_VALUE

看起来出现了一坨指令,我挨个逐步的解释下:

  1. LOAD_NAME。把本地变量中相关的值(也就是 a)放入堆栈。
  2. LOAD_CONST。把字节码中用到的对应常量 (也就是 2) 放入堆栈。
  3. DUP_TOP_TWO。复制栈顶中前 2 个引用(也就是 a 和 2),并保留顺序。
  4. BINARY_SUBSCR。把 a [2] 放到栈顶。
  5. LOAD_CONST。再分别把 5 和 6 放入堆栈。
  6. BUILD_LIST。 根据目前堆栈包含的数量创建一个列表,并放入堆栈。
  7. INPLACE_ADD。a += b 其实就是 a = a + b,也就是对栈顶做 in-place add 的操作。
  8. ROT_THREE。把堆栈中的第二和第三升高,把栈顶(也就是 [3, 4, 5, 6])降到栈中的第三位。
  9. STORE_SUBSCR。就是执行 a [2] = [3, 4, 5, 6]。但是由于 tuple 不可变,这步失败了。

可以看到执行的过程,是先对列表进行了 iadd 操作并且成功,而之后的 tuple 赋值失败报错。

也就是:

x = a[2] 
x = x.__iadd__([5, 6])
a[2] = x

这样。验证下:

In : a = (1, 2, [3, 4])
In : a[2] = [3, 4, 5, 6]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-31-d5ba6baf4cf6> in <module>()
----> 1 a[2] = [3, 4, 5, 6]

TypeError: 'tuple' object does not support item assignment

In : a
Out: (1, 2, [3, 4])

可以看到直接赋值的没有成功。

在 Python 中,变量赋值采用对象引用的方式,传递的是一个对象的内存地址(像一个指针)。在这里 a 各项指向了内存中储存了不同数据的实体,对 list 实体的修改会成功:

In : b = [3, 4]
In : a = (1, 2, b)
In : id(b)
Out: 4571378504

In : a[2] += [5, 6]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-16-84fb4a701b92> in <module>()
----> 1 a[2] += [5, 6]

TypeError: 'tuple' object does not support item assignment

In : id(b)
Out: 4571378504

In : a
Out: (1, 2, [3, 4, 5, 6])

可以看到 b 在值被改变之后,还是原来的那个对象。但是对于其他项的修改就不成功:

In : a[1] += 1
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-25-9fac1c91b625> in <module>()
----> 1 a[1] += 1

TypeError: 'tuple' object does not support item assignment

In : a
Out: (1, 2, [3, 4, 5, 6])

这是因为数值型(number)、字符串 (string) 均为不可变的对象。而字典也可以修改成功:

In : a = (1, 2, {'b': 1})
In : a[2]['b'] += 3

In : a
Out: (1, 2, {'b': 4})

竟然没有报错就成功了。我们再直接赋值看看:

In : a[2] = {'b': 5}
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-36-a2916525c596> in <module>()
----> 1 a[2] = {'b': 5}

TypeError: 'tuple' object does not support item assignment

In : a
Out: (1, 2, {'b': 4})

所以a[2]['b'] += 3并不是对元组的赋值,而是直接操作了元组中的字典项了。感受下:

In : a = (1, 2, {'b': 1})

In : dis.dis("a[2]['b'] += 5")
          0 STORE_GLOBAL    12891 (12891)
          3 FOR_ITER        10075 (to 10081)
          6 DELETE_GLOBAL   23847 (23847)
          9 SLICE+2
         10 STORE_SLICE+3
         11 DELETE_SUBSCR
         12 SLICE+2
         13 DELETE_SLICE+3

In : c = a[2]
In : c
Out: {'b': 1}

In : dis.dis("c['b'] += 5")
          0 DUP_TOPX        10075
          3 DELETE_GLOBAL   23847 (23847)
          6 SLICE+2
          7 STORE_SLICE+3
          8 DELETE_SUBSCR
          9 SLICE+2
         10 DELETE_SLICE+3

看到了吧,c 是一个 dict,对c['b'] += 5"操作的字节码指令和a[2]['b'] += 5的下面绝大部分的指令一样。