一. 對序列使用+和*
Python 程序員會默認序列是支持 + 和 * 操作的。通常 + 號兩側的序列由相同類型的數據所構成,在拼接的過程中,兩個被操作的序列都不會被修改,Python 會新建一個包含同樣類型數據的序列來作爲拼接的結果。
如果想要把一個序列複製幾份然後再拼接起來,更快捷的做法是把這個序列乘以一個整數。+ 和 * 都遵循這個規律,不修改原有的操作對象,而是構建一個全新的序列。
如果在 a * n 這個語句中,序列 a 裏的元素是對其他可變 對象的引用的話,你就需要格外注意了,因爲這個式子的結果可能 會出乎意料。比如,你想用 my_list = [[]] * 3 來初始化一個 由列表組成的列表,但是你得到的列表裏包含的 3 個元素其實是 3 個引用,而且這 3 個引用指向的都是同一個列表。
演示1 建立由列表組成的列表
上述操作等同於(每次迭代中新建了一個list。作爲新的一行添加到board中):
演示2 含有3個指向同一對象的引用的列表是毫無用處的
上述錯誤示範等同於(追加同一個對象3次到board列表):
二. 序列的增量賦值
2.1 +=和*=背後的故事
增量賦值運算符 += 和 *= 的表現取決於它們的第一個操作對象。+= 背後的特殊方法是 __iadd__ (用於“就地加法”)。但是如果一個類沒有實現這個方法的話,Python 會退一步調用 __add__ 。如:在a += b的執行過程中,如果 a 實現了 __iadd__ 方法,就會調用這個方法。同時對可變序列來說,a 會就地改動,就像調用了 a.extend(b) 一樣。但是如果 a 沒有實現 __iadd__ 的話,a += b 這個表達式的效果就變得跟 a = a + b 一樣了:首先計算 a + b,得到一個新的對象,然後賦值給 a。也就是說,在這個表達式中,變量名會不會被關聯到新的對象,完全取決於這個類型有沒有實現 __iadd__ 這個方法。
總體來講,可變序列一般都實現了 __iadd__ 方法,因此 += 是就地加 法。而不可變序列根本就不支持這個操作,對這個方法的實現也就無從 談起。 上面所說的這些關於 += 的概念也適用於 *=,不同的是,後者相對應的 是 __imul__。
演示3 *=在可變和不可變序列上的對比
對不可變序列進行重複拼接操作的話,效率會很低,因爲每次都有一個新對象,而解釋器需要把原來對象中的元素先複製到新的對象裏,然後再追加新的元素。
str 是一個例外,因爲對字符串做 += 實在是太普遍了,所以 CPython 對它做了優化。爲 str 初始化內存的時候,程序會爲它留出額外的可擴展空間,因此進行增量操作的時候,並不會涉及複製原有字符串到新位置這類操作。
2.2 一個關於+=的謎題
上述代碼中,因爲 tuple 不支持對它的元素賦值,所以會拋出 TypeError 異常。但與此同時元組卻被改變了:t= (1, 2, [30, 40, 50, 60])。這是非常罕見的邊界情況,因此我們需要注意以下3點:
- 不要把可變對象放在元組裏面。
- 增量賦值不是一個原子操作。我們剛纔也看到了,它雖然拋出了異常,但還是完成了操作。
三. list.sort方法和內置函數sorted
list.sort 方法會就地排序列表,也就是說不會把原列表複製一份。這也是這個方法的返回值是 None 的原因。在這種情況下返回 None 其實是 Python 的一個慣例:如果一個函數或者方法對對象進行的是就地改動,那它就應該返回 None,好讓調用者知道傳入的參數發生了變動,而且並未產生新的對象。例如,random.shuffle 函數也遵守了這個慣例。(注:用返回 None 來表示就地改動這個慣例有個弊端,那就是調用者無法將其串聯起來。)
與 list.sort 相反的是內置函數 sorted,它會新建一個列表作爲返回值。這個方法可以接受任何形式的可迭代對象作爲參數,甚至包括不可變序列或生成器。而不管 sorted 接受的是怎樣的參數,它最後都會返回一個列表。
不管是 list.sort 方法還是 sorted 函數,都有兩個可選的關鍵字參數:
- reverse:如果被設定爲 True,被排序的序列裏的元素會以降序輸出;默認值是 False升序。
- key:一個只有一個參數的函數,這個函數會被用在序列裏的每一個元素上,所產生的結果將是排序算法依賴的對比關鍵字。這個參數的默認值是恆等函數(identity function),也就是默認用元素自己的值來排序。
演示4 list.sort()和sorted()的用法