序列的增量賦值
增量賦值運算符 += 和 *= 的表現取決于它們的第一個操作對象。簡單起
見,我們把討論集中在增量加法(+=)上,但是這些概念對 *= 和其他
增量運算符來說都是一樣的。
+= 背后的特殊方法是 iadd (用于“就地加法”)。但是如果一個類
沒有實現這個方法的話,Python 會退一步調用 add 。考慮下面這
個簡單的表達式:
>>> a += b
如果 a 實現了 iadd 方法,就會調用這個方法。同時對可變序列
(例如 list、bytearray 和 array.array)來說,a 會就地改動,就
像調用了 a.extend(b) 一樣。但是如果 a 沒有實現 iadd 的話,a
+= b 這個表達式的效果就變得跟 a = a + b 一樣了:首先計算 a +
b,得到一個新的對象,然后賦值給 a。也就是說,在這個表達式中,
變量名會不會被關聯到新的對象,完全取決于這個類型有沒有實現
iadd 這個方法。
總體來講,可變序列一般都實現了 iadd 方法,因此 += 是就地加
法。而不可變序列根本就不支持這個操作,對這個方法的實現也就無從
談起。
上面所說的這些關于 += 的概念也適用于 *=,不同的是,后者相對應的
是 imul。關于 iadd 和 imul,接下來有個小例子,展示的是 *= 在可變和不可變序列上的作用:
>>> l = [1, 2, 3]
>>> id(l)
4311953800 ?
>>> l *= 2
>>> l
[1, 2, 3, 1, 2, 3]
>>> id(l)
4311953800 ?
>>> t = (1, 2, 3)
>>> id(t)
4312681568 ?
>>> t *= 2
>>> id(t)
4301348296 ?
? 剛開始時列表的 ID。
? 運用增量乘法后,列表的 ID 沒變,新元素追加到列表上。
? 元組最開始的 ID。
? 運用增量乘法后,新的元組被創建。
對不可變序列進行重復拼接操作的話,效率會很低,因為每次都有一個
新對象,而解釋器需要把原來對象中的元素先復制到新的對象里,然后
再追加新的元素。
str 是一個例外,因為對字符串做 += 實在是太普遍了,所以 CPython 對它做了優化。為 str
初始化內存的時候,程序會為它留出額外的可擴展空間,因此進行增量操作的時候,并不會涉
及復制原有字符串到新位置這類操作。
我們已經認識了 += 的一般用法,下面來看一個有意思的邊界情況。這
個例子可以說是突出展示了“不可變性”對于元組來說到底意味著什么。
一個關于+=的謎題
讀讀完下面的代碼,然后回答這個問題:示例 2-14 中的兩個表達式到底
會產生什么結果? 回答之前不要用控制臺去運行這兩個式子。讀完下面的代碼,然后回答這個問題:示例 2-14 中的兩個表達式到底
會產生什么結果? 回答之前不要用控制臺去運行這兩個式子。
示例 2-14 一個謎題
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
到底會發生下面 4 種情況中的哪一種?
a. t 變成 (1, 2, [30, 40, 50, 60])。
b. 因為 tuple 不支持對它的元素賦值,所以會拋出 TypeError 異常。
c. 以上兩個都不是。
d. a 和 b 都是對的。
我剛看到這個問題的時候,異常確定地選擇了 b,但其實答案是 d,也
就是說 a 和 b 都是對的!示例 2-15 是運行這段代碼得到的結果,用的
Python 版本是 3.4,但是在 2.7 中結果也一樣。
示例 2-15 沒人料到的結果:t[2] 被改動了,但是也有異常拋出
>>> 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])
Python Tutor(http://www.pythontutor.com)是一個對 Python 運行原理進行
可視化分析的工具。圖 2-3 里是兩張截圖,分別代表示例 2-15 中 t 的
初始和最終狀態。
下面來看看示例 2-16 中 Python 為表達式 s[a] += b 生成的字節碼,可
能這個現象背后的原因會變得清晰起來。
示例 2-16 s[a] = b 背后的字節碼
>>> dis.dis('s[a] += b')
1 0 LOAD_NAME 0(s)
3 LOAD_NAME 1(a)
6 DUP_TOP_TWO
7 BINARY_SUBSCR ?
8 LOAD_NAME 2(b)
11 INPLACE_ADD ?
12 ROT_THREE
13 STORE_SUBSCR ?
14 LOAD_CONST 0(None)
17 RETURN_VALUE
? 將 s[a] 的值存入 TOS(Top Of Stack,棧的頂端)。
? 計算 TOS += b。這一步能夠完成,是因為 TOS 指向的是一個可變對
象(也就是示例 2-15 里的列表)。
? s[a] = TOS 賦值。這一步失敗,是因為 s 是不可變的元組(示例 2-15 中的元組 t)。
這其實是個非常罕見的邊界情況,在 15 年的 Python 生涯中,我還沒見
過誰在這個地方吃過虧。
至此我得到了 3 個教訓。
不要把可變對象放在元組里面。
增量賦值不是一個原子操作。我們剛才也看到了,它雖然拋出了異
常,但還是完成了操作。
查看 Python 的字節碼并不難,而且它對我們了解代碼背后的運行機
制很有幫助。
在見證了 + 和 * 的微妙之處后,我們把話題轉移到序列類型的另一個重
要部分上:排序。