python 增强赋值 "+=" 操作在不可变对象中使用的问题
针对元组中的列表,有如下三种操作:
>>> a = ([], [])
>>> a[0].append(1)
>>> a[0].extend([2])
>>> a[0] += [3]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
针对这个报错的原因,我们可以先看一下这个操作的字节码是什么。
我们将其写成函数:
>>> def func():
... a = ([], [])
... a[0].append(1)
... a[0].extend([2])
... a[0] += [3]
...
>>> import dis
>>> dis.dis(func)
运行后显示的字节码是这样的:
2 0 BUILD_LIST 0
2 BUILD_LIST 0
4 BUILD_TUPLE 2
6 STORE_FAST 0 (a)
3 8 LOAD_FAST 0 (a)
10 LOAD_CONST 1 (0)
12 BINARY_SUBSCR
14 LOAD_ATTR 0 (append)
16 LOAD_CONST 2 (1)
18 CALL_FUNCTION 1
20 POP_TOP
4 22 LOAD_FAST 0 (a)
24 LOAD_CONST 1 (0)
26 BINARY_SUBSCR
28 LOAD_ATTR 1 (extend)
30 LOAD_CONST 3 (2)
32 BUILD_LIST 1
34 CALL_FUNCTION 1
36 POP_TOP
5 38 LOAD_FAST 0 (a)
40 LOAD_CONST 1 (0)
42 DUP_TOP_TWO
44 BINARY_SUBSCR
46 LOAD_CONST 4 (3)
48 BUILD_LIST 1
50 INPLACE_ADD
52 ROT_THREE
54 STORE_SUBSCR
56 LOAD_CONST 0 (None)
58 RETURN_VALUE
其中,第一列是 python 代码行号,第二列是字节码的起始位置,圆括号中是操作的值。
我们看到,一行 python 代码,编译成字节码后有多条指令。其中 "+=" 操作有 9 条指令(最后两条为函数的返回值)。
下面详细解释一下这些指令,在这个过程中,大家可以画一下栈的状态,这样更容易看出问题所在。
各字节码指令的含义参考:https://docs.python.org/3/library/dis.html
LOAD_FAST 0 (a)
这条指令把局部变量 a 的值放入栈
LOAD_CONST 1 (0)
这条指令从保存常量的地方将值 0 取出,放入栈中
DUP_TOP_TWO
这条指令复制出栈顶的两个对象,也就是a
和0
,然后放入栈中,并保持其顺序不变
BINARY_SUBSCR
实现 TOS = TOS1[TOS] 的操作,TOS 代表栈顶元素,TOS1 代表栈顶下一个元素,以此类推。按现在的数据,就是取出a
和0
,执行a[0]
,把结果放回栈中。现在栈的情况是a, 0, a[0]
,右边是栈顶
LOAD_CONST 4 (3)
加载常量 3,放入栈顶,现在栈的情况是a, 0, a[0], 3
BUILD_LIST 1
取出栈顶元素,组成 list 对象,并放回栈中,现在栈的情况是a, 0, a[0], [3]
INPLACE_ADD
原地实现 TOS = TOS1 + TOS,即a[0] + [3]
,结果就是a[0]
变成了[1, 2, 3]
,现在栈的情况是a, 0, [1, 2, 3]
ROT_THREE
将第二个和第三个元素往上移动一个位置,将栈顶元素放在第三个位置,即[1, 2, 3], a, 0
STORE_SUBSCR
实现 TOS1[TOS] = TOS2,即a[0] = [1, 2, 3]
,这时候问题就来了,对于元组这种不可变对象,是不支持改变元素的
错误还可以这样找 可以学到了