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

  1. LOAD_FAST 0 (a)
    这条指令把局部变量 a 的值放入栈

  2. LOAD_CONST 1 (0)
    这条指令从保存常量的地方将值 0 取出,放入栈中

  3. DUP_TOP_TWO
    这条指令复制出栈顶的两个对象,也就是a0,然后放入栈中,并保持其顺序不变

  4. BINARY_SUBSCR
    实现 TOS = TOS1[TOS] 的操作,TOS 代表栈顶元素,TOS1 代表栈顶下一个元素,以此类推。按现在的数据,就是取出a0,执行a[0],把结果放回栈中。现在栈的情况是a, 0, a[0],右边是栈顶

  5. LOAD_CONST 4 (3)
    加载常量 3,放入栈顶,现在栈的情况是a, 0, a[0], 3

  6. BUILD_LIST 1
    取出栈顶元素,组成 list 对象,并放回栈中,现在栈的情况是a, 0, a[0], [3]

  7. INPLACE_ADD
    原地实现 TOS = TOS1 + TOS,即a[0] + [3],结果就是a[0]变成了[1, 2, 3],现在栈的情况是a, 0, [1, 2, 3]

  8. ROT_THREE
    将第二个和第三个元素往上移动一个位置,将栈顶元素放在第三个位置,即[1, 2, 3], a, 0

  9. STORE_SUBSCR
    实现 TOS1[TOS] = TOS2,即a[0] = [1, 2, 3],这时候问题就来了,对于元组这种不可变对象,是不支持改变元素的