Python中tuple+=赋值的四个问题

原文链接

最近偶尔翻看Fluent Python,遇到有意思的东西就记下来. 下面的是在PyCon2013上提出的一个关于tuple的Augmented Assignment也就是增量赋值的一个问题。 并且基于此问题, 又引申出3个变种问题.

问题

首先看第一个问题, 如下面的代码段:

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

会产生什么结果呢? 给出了四个选项:

  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

>>> 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还是变了?
再看第二种情况,稍微变化一下,将+=变为=:

>>> 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来作为元素的值,然后对ain-place的修改,其中并没有涉及到对tuple的assign操作,那肯定是正常执行的。

总结

这个问题其实以前也就遇到过,但是没想过具体的原理,后来翻书的时候又看到了, 于是花了点时间把这一个系列查了部分资料以及结合自己的理解都整理了出来, 算是饭后茶点吧, 不严谨的地方烦请指出.

部分参考如下:


shomy
记录学习

Life is like a box of chocolates

1.6k 声望
42 粉丝
0 条评论
推荐阅读
最好用的 python 库合集
🎈 分词 - jieba优秀的中文分词库,依靠中文词库,利用词库确定汉子之间关联的概率,形成分词结果 {代码...} 🎈 词云库 - wordcloud对数据中出现频率较高的 关键词 生成的一幅图像,予以视觉上的突出 {代码...} 🎈 ...

tiny极客11阅读 2.8k评论 2

封面图
数据结构与算法:二分查找
一、常见数据结构简单数据结构(必须理解和掌握)有序数据结构:栈、队列、链表。有序数据结构省空间(储存空间小)无序数据结构:集合、字典、散列表,无序数据结构省时间(读取时间快)复杂数据结构树、 堆图二...

白鲸鱼9阅读 5.3k

滚蛋吧,正则表达式!
你是不是也有这样的操作,比如你需要使用「电子邮箱正则表达式」,首先想到的就是直接百度上搜索一个,然后采用 CV 大法神奇地接入到你的代码中?

良许3阅读 1.5k

搭个ChatGPT算法模型,从哪开始?
最近 ChatGPT 很火,火到了各行各业。记得去年更多的还是码农最新体验后拿它搜代码,现在各行各业都进来体验,问它咋理财、怎么写报告和给小孩起名。😂 也因此让小傅哥在头条的一篇关于 ChatGPT 的文章都有了26万...

小傅哥6阅读 1.2k

封面图
程序员适合创业吗?
大家好,我是良许。从去年 12 月开始,我已经在视频号、抖音等主流视频平台上连续更新视频到现在,并得到了不错的评价。每个视频都花了很多时间精力用心制作,欢迎大家关注哦~考虑到有些小伙伴没有看过我的视频,...

良许3阅读 1.3k

Ubuntu20.04 从源代码编译安装 python3.10
Ubuntu 22.04 Release DateUbuntu 22.04 Jammy Jellyfish is scheduled for release on April 21, 2022If you’re ready to use Ubuntu 22.04 Jammy Jellyfish, you can either upgrade your current Ubuntu syste...

ponponon1阅读 4.5k评论 1

PyCharm 激活破解教程, 2023 年 2 月亲测有用
本文分享一下PyCharm 2022.2.3 版本最新激活破解教程,注意不要使用太新的版本,都是 Jetbrains 产品,本文专门配上了 Pycharm 的图片,跟着下面教程一步一步来即可。

程序员徐公阅读 8.8k评论 1

Life is like a box of chocolates

1.6k 声望
42 粉丝
宣传栏