Python中tuple+=賦值的四個問題

原文連接html

最近偶爾翻看Fluent Python,遇到有意思的東西就記下來. 下面的是在PyCon2013上提出的一個關於tuple的Augmented Assignment也就是增量賦值的一個問題。 而且基於此問題, 又引伸出3個變種問題.python

問題

首先看第一個問題, 以下面的代碼段:app

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

會產生什麼結果呢? 給出了四個選項:函數

  1. t 變成 [1,2, [30,40,50,60]spa

  2. TypeError is raised with the message 'tuple' object does not support item assignmentcode

  3. Neither 1 nor 2htm

  4. Both 1 and 2對象

按照以前的理解, tuple裏面的元素是不能被修改的,所以會選2. 若是真是這樣的話,這篇筆記就不必了,Fluent Python中也就不會拿出一節來說了。 正確答案是4get

>>> 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來做爲元素的值,而後對ain-place的修改,其中並無涉及到對tuple的assign操做,那確定是正常執行的。

總結

這個問題其實之前也就遇到過,可是沒想過具體的原理,後來翻書的時候又看到了, 因而花了點時間把這一個系列查了部分資料以及結合本身的理解都整理了出來, 算是飯後茶點吧, 不嚴謹的地方煩請指出.

部分參考以下:

相關文章
相關標籤/搜索