Python元组的赋值谜题
/ / / 阅读数:3300相信很多人对 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 |
看起来出现了一坨指令,我挨个逐步的解释下:
- LOAD_NAME。把本地变量中相关的值(也就是 a)放入堆栈。
- LOAD_CONST。把字节码中用到的对应常量 (也就是 2) 放入堆栈。
- DUP_TOP_TWO。复制栈顶中前 2 个引用(也就是 a 和 2),并保留顺序。
- BINARY_SUBSCR。把 a [2] 放到栈顶。
- LOAD_CONST。再分别把 5 和 6 放入堆栈。
- BUILD_LIST。 根据目前堆栈包含的数量创建一个列表,并放入堆栈。
- INPLACE_ADD。
a += b
其实就是a = a + b
,也就是对栈顶做 in-place add 的操作。 - ROT_THREE。把堆栈中的第二和第三升高,把栈顶(也就是 [3, 4, 5, 6])降到栈中的第三位。
- 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
的下面绝大部分的指令一样。