Python中tuple+=赋值的四个问题
花下猫语:上周末,我终于翻译完了《PEP255--简单的生成器》,有同学建议我把后续的相关 PEP 也一起翻译了。我当然有此想法。不只是生成器相关的 PEP,若有余力允许,我还准备偶尔花时间,把其它重要的 PEP 翻译一些呢。不过由于时间、精力与知识深/广度的原因,这件事可急不来。闲聊结束。今天,给大家分享一篇好文章。
原文:http://shomy.top/2017/08/17/python-tuple-assign
作者:ShomyLiu
(本文经原作者授权转载,略有改动)
最近偶尔翻看《Fluent Python》,遇到有意思的东西就记下来。下面的是在PyCon2013
上提出的一个关于 tuple 的Augmented Assignment
也就是增量赋值的一个问题。 并且基于此问题, 又引申出3个变种问题。
问题
首先看第一个问题, 如下面的代码段:
1,2, [30,40])
> t[2] += [50,60]
> t = (会产生什么结果呢? 给出四个选项:
1. `t` 变成 `[1,2, [30,40,50,60]`
2. `TypeError is raised with the message 'tuple' object does not support item assignment`
3. Neither 1 nor 2
4. Both 1 and 2
按照之前的理解, tuple
里面的元素是不能被修改的,因此会选 2.
如果真是这样的话,这篇笔记就没必要了,《Fluent Python》也就不会拿出一节来讲了。
正确答案是 4 :
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 = (问题来了,为什么异常都出来了,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])
> t = (再看第三种情况,只把+=
换为extend
或者append
:
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])
> t = (又正常了,没抛出异常?
最后第四种情况,用变量的形式:
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 = [又是一种情况,下面就探究一下其中的原因。
原因
首先需要重温+=
这个运算符,如a+=b
:
对于可变对象(mutable object)如
list
,+=
操作的结果会直接在a
对应的变量进行修改,而a
对应的地址不变.对于不可变对象(imutable object)如
tuple
,+=
则是等价于a = a+b
会产生新的变量,然后绑定到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 # 地址变化了
> a = [此外还需要注意的是, 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并没有变化,看看下面的例子:
1,2,[30,40])
> id(a[2])
140628739513736
> a[2].extend([50,60])
> a
(1, 2, [30, 40, 50, 60])
> id(a[2])
140628739513736
> a=(目前解决了第二个和第三个问题, 先梳理一下, 其实就是两点:
tuple内部的元素不支持赋值操作
在第一条的基础上, 如果元素的
id
没有变化, 元素其实是可以改变的.
现在再来看最初的第一个问题: t[2] += [50,60]
按照上面的结论, 不应该抛异常啊,因为在我们看来+=
对于可变对象t[2]
来说, 属于in-place
操作,也就是直接修改自身的内容, id
并不变, 确认下id并没有变化:
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
> a=(跟第三个问题仅仅从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
操作,那肯定是正常执行的。
总结
这个问题其实以前也就遇到过,但是没想过具体的原理,后来翻书的时候又看到了,于是花了点时间把这一个系列查了部分资料以及结合自己的理解都整理了出来, 算是饭后茶点吧, 不严谨的地方烦请指出.
随机推荐,偶遇精彩
1
2
3
4
一只伪喵星来客
一个有趣又有用的学习分享平台
专注Python技术、数据科学和深度学习
兼具极客思维与人文情怀
欢迎你关注
微信号:python_cat