关于python:Python中tuple赋值的四个问题

39次阅读

共计 3661 个字符,预计需要花费 10 分钟才能阅读完成。

最近偶然翻看《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 来作为元素的值,而后对 a 做 in-place 的批改,其中并没有波及到对 tuple 的 assign 操作,那必定是失常执行的。

以上就是本次分享的所有内容,想要理解更多 python 常识欢送返回公众号:Python 编程学习圈,发送“J”即可收费获取,每日干货分享

正文完
 0