quiz

今天在twitter上看到一个有趣的问题。

t = (1, 2, [30, 40])
t[2] += [50, 60]

结果是什么?

  1. t = (1, 2, [30, 40, 50, 60])
  2. TypeError
  3. 都不是
  4. 1,2都发生

出乎意料的,结果是4。

为什么,我们来分析一下。

disassemble

>>> t = (1, 2, [30, 40])
>>> def f(): t[2] += [50, 60]
>>> import dis
>>> dis.dis(f)

以下是结果。

 LOAD_GLOBAL              0 (t)
 LOAD_CONST               1 (2)
 DUP_TOPX                 2
 BINARY_SUBSCR
 LOAD_CONST               2 (50)
 LOAD_CONST               3 (60)
 BUILD_LIST               2
 INPLACE_ADD
 ROT_THREE
 STORE_SUBSCR
 LOAD_CONST               0 (None)
 RETURN_VALUE

我们来解读一下。dis的指令集可以看这里

  1. 先载入全局变量t(因为写法问题,t是全局)。
  2. 载入常数2。现在栈是t|2。
  3. 复制头两项。现在栈应当是t|2|t|2。
  4. BINARY_SUBSCR,取t[2]。现在是t|2|t[2]。
  5. 载入50和60,然后build_list。现在是t|2|t[2]|[50,60]。
  6. INPLACE_ADD,注意这里。现在是t|2|[30,40,50,60]。同时t[2]的list成为了[30,40,50,60]。
  7. ROT_THREE,结果是[30,40,50,60]|t|2。
  8. STORE_SUBSCR,实际是在操作t[2] = [30,40,50,60]。当然,这里出错了。
  9. 最后是return None,这步并未发生。

compare with add

整个最诡异的地方,就在于INPLACE_ADD。从字面上,这要求inplace的做加法。我们换个写法,然后再看看。

>>> def f(): t[2] = t[2] + [50, 60]
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in f
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [30, 40])
>>> dis.dis(f)

以下为结果。

 LOAD_GLOBAL              0 (t)
 LOAD_CONST               1 (2)
 BINARY_SUBSCR
 LOAD_CONST               2 (50)
 LOAD_CONST               3 (60)
 BUILD_LIST               2
 BINARY_ADD
 LOAD_GLOBAL              0 (t)
 LOAD_CONST               1 (2)
 STORE_SUBSCR
 LOAD_CONST               0 (None)
 RETURN_VALUE

看到没,结果就不一样了。

why

没有为什么,因为+=对应的指令就是INPLACE_ADD。

只是INPLACE_ADD了,理论上就不需要赋值了。我没想明白的是,在什么情况下INPLACE_ADD了又不赋值可能导致错误呢?(我觉得可能有这种情况)