相信很多人对tuple和list的区别的理解是tuple是一个不可变的序列, 不能对它的元素赋值。我之前也是这么理解的,举个例子:
1 |
|
也就是一个元组生成,它的元素就不再能改变了。
但是相信很多人见过下面这样的玩法(有人把它当做Python的一个笑话):
1 |
|
明确的报错了,可是为了a的值还是改了呢?
我曾经思考过这个问题,直接上感觉是「对列表[3, 4]的赋值成功,但是后来发生的元组赋值失败造成的」,但是一直苦于没有证据。直到昨晚看《Fluent
Python》的时候,才从作者哪里获得了肯定的答案。今天我们用dis模块来分析+=所产生的bytecode(把python代码反汇编为字节码指令):
1 |
|
看起来出现了一坨指令,我挨个逐步的解释下:
- 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赋值失败报错。
也就是:
1 |
|
这样。验证下:
1 |
|
可以看到直接赋值的没有成功。
在Python中,变量赋值采用对象引用的方式,传递的是一个对象的内存地址(像一个指针)。在这里a各项指向了内存中储存了不同数据的实体,对list实体的修改会成功:
1 |
|
可以看到b在值被改变之后,还是原来的那个对象。但是对于其他项的修改就不成功:
1 |
|
这是因为数值型(number)、字符串(string)均为不可变的对象。而字典也可以修改成功:
1 |
|
竟然没有报错就成功了。我们再直接赋值看看:
1 |
|
所以a[2]['b'] += 3
并不是对元组的赋值,而是直接操作了元组中的字典项了。感受下:
1 |
|
看到了吧,c是一个dict,对c['b'] += 5"
操作的字节码指令和a[2]['b'] += 5
的下面绝大部分的指令一样。
版权声明:本文由 董伟明 原创,未经作者授权禁止任何微信公众号和向掘金(juejin.im)转载,技术博客转载采用 保留署名-非商业性使用-禁止演绎 4.0-国际许可协议
python