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

1
2
3
4
5
6
7
8
9
10
11
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的一个笑话):

1
2
3
4
5
6
7
8
9
10
11
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代码反汇编为字节码指令):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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赋值失败报错。

也就是:

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

这样。验证下:

1
2
3
4
5
6
7
8
9
10
11
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实体的修改会成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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在值被改变之后,还是原来的那个对象。但是对于其他项的修改就不成功:

1
2
3
4
5
6
7
8
9
10
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)均为不可变的对象。而字典也可以修改成功:

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

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

1
2
3
4
5
6
7
8
9
10
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并不是对元组的赋值,而是直接操作了元组中的字典项了。感受下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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的下面绝大部分的指令一样。