原文连接html
最近偶尔翻看Fluent Python,遇到有意思的东西就记下来. 下面的是在PyCon2013上提出的一个关于
tuple
的Augmented Assignment也就是增量赋值的一个问题。 而且基于此问题, 又引伸出3个变种问题.python
首先看第一个问题, 以下面的代码段:app
>>> t = (1,2, [30,40]) >>> t[2] += [50,60]
会产生什么结果呢? 给出了四个选项:函数
t
变成 [1,2, [30,40,50,60]
spa
TypeError is raised with the message 'tuple' object does not support item assignment
code
Neither 1 nor 2htm
Both 1 and 2对象
按照以前的理解, tuple
里面的元素是不能被修改的,所以会选2
. 若是真是这样的话,这篇笔记就不必了,Fluent Python中也就不会拿出一节来说了。 正确答案是4
get
>>> t = (1,2,[30,40]) >>> t[2] += [50,60] Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'tuple' object does not support item assignment >>> t (1, 2, [30, 40, 50, 60])
问题来了,为何异常都出来了, t
仍是变了?
再看第二种状况,稍微变化一下,将+=
变为=
:it
>>> t = (1,2, [30,40]) >>> t[2] = [50,60]
结果就成酱紫了:
>>> t = (1,2, [30,40]) >>> t[2] = [50,60] Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'tuple' object does not support item assignment >>> t (1, 2, [30, 40])
再看第三种状况,只把+=
换为extend
或者append
,:
>>> t = (1, 2, [30,40]) >>> t[2].extend([50,60]) >>> t (1, 2, [30, 40, 50, 60]) >>> t[2].append(70) >>> t (1, 2, [30, 40, 50, 60, 70])
又正常了,没抛出异常?
最后第四种状况, 用变量的形式:
>>> a = [30,40] >>> t = (1, 2, a) >>> a+=[50,60] >>> a [30, 40, 50, 60] >>> t (1, 2, [30, 40, 50, 60]) >>> t[2] += [70,80] Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'tuple' object does not support item assignment >>> t (1, 2, [30, 40, 50, 60, 70, 80])
又是一种状况, 下面就探究一下其中的缘由.
首先须要重温+=
这个运算符,如a+=b
:
对于可变对象(mutable object)如list
, +=
操做的结果会直接在a
对应的变量进行修改,而a
对应的地址不变.
对于不可变对象(imutable object)如tuple
, +=
则是等价于a = a+b
会产生新的变量,而后绑定到a
上而已.
以下代码段, 能够看出来:
>>> a = [1,2,3] >>> id(a) 53430752 >>> a+=[4,5] >>> a [1, 2, 3, 4, 5] >>> id(a) 53430752 # 地址没有变化 >>> b = (1,2,3) >>> id(b) 49134888 >>> b += (4,5) >>> b (1, 2, 3, 4, 5) >>> id(b) 48560912 # 地址变化了
此外还须要注意的是, python中的tuple
做为不可变对象, 也就是咱们平时说的元素不能改变, 实际上从报错信息TypeError: 'tuple' object does not support item assignment
来看, 更准确的说法是指其中的元素不支持赋值操做=
(assignment).
先看最简单的第二种状况, 它的结果是符合咱们的预期, 由于=
产生了assign
的操做.(在由一个例子到python的名字空间 中指出了赋值操做=
就是建立新的变量), 所以s[2]=[50,60]
就会抛出异常.
再看第三种状况,包含extend/append
的, 结果tuple中的列表值发生了变化,可是没有异常抛出. 这个其实也相对容易理解. 由于咱们知道tuple
中存储的实际上是元素所对应的地址(id), 所以若是没有赋值操做且tuple中的元素的id
不变,便可,而list.extend/append
只是修改了列表的元素,而列表自己id并无变化,看看下面的例子:
>>> a=(1,2,[30,40]) >>> id(a[2]) 140628739513736 >>> a[2].extend([50,60]) >>> a (1, 2, [30, 40, 50, 60]) >>> id(a[2]) 140628739513736
目前解决了第二个和第三个问题, 先梳理一下, 其实就是两点:
tuple内部的元素不支持赋值操做
在第一条的基础上, 若是元素的id
没有变化, 元素实际上是能够改变的.
如今再来看最初的第一个问题: t[2] += [50,60]
按照上面的结论, 不该该抛异常啊,由于在咱们看来+=
对于可变对象t[2]
来讲, 属于in-place
操做,也就是直接修改自身的内容, id
并不变, 确认下id并无变化:
>>> a=(1,2,[30,40]) >>> id(a[2]) 140628739587392 >>> a[2]+=[50,60] Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'tuple' object does not support item assignment >>> a (1, 2, [30, 40, 50, 60]) >>> id(a[2]) # ID 并无发生改变 140628739587392
跟第三个问题仅仅从t[2].extend
改为了t[2]+=
, 就抛出异常了,因此问题应该是出在+=
上了.
下面用dis
模块看看它俩执行的步骤:
对下面的代码块执行dis
:
t = (1,2, [30,40]) t[2] += [50,60] t[2].extend([70, 80])
执行python -m dis test.py
,结果以下,下面只保留第2,3行代码的执行过程,以及关键步骤的注释以下:
2 21 LOAD_NAME 0 (t) 24 LOAD_CONST 1 (2) 27 DUP_TOPX 2 30 BINARY_SUBSCR 31 LOAD_CONST 4 (50) 34 LOAD_CONST 5 (60) 37 BUILD_LIST 2 40 INPLACE_ADD 41 ROT_THREE 42 STORE_SUBSCR 3 43 LOAD_NAME 0 (t) 46 LOAD_CONST 1 (2) 49 BINARY_SUBSCR 50 LOAD_ATTR 1 (extend) 53 LOAD_CONST 6 (70) 56 LOAD_CONST 7 (80) 59 BUILD_LIST 2 62 CALL_FUNCTION 1 65 POP_TOP 66 LOAD_CONST 8 (None) 69 RETURN_VALUE
解释一下关键的语句:
30 BINARY_SUBSCR
: 表示将t[2]
的值放在TOS(Top of Stack),这里是指[30, 40]
这个列表
40 INPLACE_ADD
: 表示TOS += [50,60]
执行这一步是能够成功的,修改了TOS的列表为[30,40,50,60]
42 STORE_SUBSCR
: 表示s[2] = TOS
问题就出在这里了,这里产生了一个赋值操做,所以会抛异常!可是上述对列表的修改已经完成, 这也就解释了开篇的第一个问题。
再看extend
的过程,前面都同样,只有这一行:
62 CALL_FUNCTION
: 这个直接调用内置extend函数完成了对原列表的修改,其中并无assign
操做,所以能够正常执行。
如今逐渐清晰了, 换句话说,+=
并非原子操做,至关于下面的两步:
t[2].extend([50,60]) t[2] = t[2]
第一步能够正确执行,可是第二步有了=
,确定会抛异常的。 一样这也能够解释在使用+=
的时候,为什么t[2]
的id
明明没有变化,可是仍然抛出异常了。
如今用一句话总结下:
tuple中元素不支持
assign
操做,可是对于那些是可变对象的元素如列表,字典等,在没有assign
操做的基础上,好比一些in-place
操做,是能够修改内容的
能够用第四个问题来简单验证一下,使用一个指向[30,40]
的名称a
来做为元素的值,而后对a
作in-place
的修改,其中并无涉及到对tuple的assign
操做,那确定是正常执行的。
这个问题其实之前也就遇到过,可是没想过具体的原理,后来翻书的时候又看到了, 因而花了点时间把这一个系列查了部分资料以及结合本身的理解都整理了出来, 算是饭后茶点吧, 不严谨的地方烦请指出.
部分参考以下:
Fluent Python