fluent python 讀書筆記 2–Python的序列型別2

futureshine發表於2019-02-20

對 Python 中的序列型別進行操作是我們的必要需求。尤其是切片,以及從列表中建立一個新的列表等操作尤其需求的多。閱讀這一部分,我收穫很多。PS: 這篇部落格有點長,一下看不完就請收藏吧。。。

切片

list, tuple, str 以及 Python 中的所有序列型別都支援切片操作,但是他們實際能做的事情比我們想象的多很多

為什麼切片和 range 函式都不包括最後一個元素

Pythonic 的慣例是不包含切片中的最後一個元素的,這和 Python, C 語言中的用 0 作為位置索引的第一位是相吻合的。這些慣例包括:

  • 當只給了切片上限的時候,可以很容易的看出切片和 range 函式的長度。無論是函式 range(3) 還是 list[:3] 所得到的內容長度均為 3
  • 當切片上下限都給了的時候,內容長度也很容易得到 stop – start ,即上限減去下限
  • 把序列按照索引 x 分開而不發生重疊的方法很簡單, list[:x]list[x:] :例如
l = [10, 20, 30, 40, 50, 60]
l[:2] # [10, 20]
l[2:] # [30, 40, 50, 60]複製程式碼

切片物件

s[a:b:c] 可以用來指明切片的步長 c ,使得目標切片跳過相應的元素。切片的步長也可以為負,這將導致切片的方向為負方向。

s = `bicycle`
s[::3] # `bye`
s[::-1] # `elcycib`
s[::-2] # `eccb`複製程式碼

a:b:c 只有位於 [] 內才生效,用來產生一個切片物件。當執行 seq[start:stop:step] 的時候, Python 會呼叫 seq.__getitem__(slice(start, stop, step)) 。即使你沒有在你自定義的序列型別中實現這個方法,知道切片物件是怎麼產生作用,也是很有用的。

如果需要處理偏平的資料 (flat-like data),如例子2-11所示的那樣,為了處理的更加明確,可以直接將對應的切片命名。這對於可讀性來說,是很有幫助的。

# 例子2-11,處理偏平資料
invoice = """
... 0.....6.................................40........52...55........
... 1909 Pimoroni PiBrella $17.50 3 $52.50
... 1489 6mm Tactile Switch x20 $4.95 2 $9.90
... 1510 Panavise Jr. - PV-201 $28.00 1 $28.00
... 1601 PiTFT Mini Kit 320x240 $34.95 1 $34.95
... """
SKU = slice(0, 6)
DESCRIPTION = slice(6, 60)
UNIT_PRICE = slice(40, 52)
QUANTITY = slice(52, 55)
ITEM_TOTAL = slice(55, None)
line_items = invoice.split(`
`)[2:]
for item in line_items:
    print item[UNIT_PRICE], item[DESCRIPTION]複製程式碼

將對應的切片命名,可以便於我們閱讀,找到我們需要提取的資訊。

多維切片和省略號

在第三方包 NumPy 中,可以使用 [] 操作符操作用逗號分隔的多個索引值或者切片來獲取元素。最簡單的操作便是 a[i, j] ,其中 a 的型別為 numpy.ndarray 。二維的切片操作為 a[m:n, k:l]__getitem____setitem__ 特殊方法就是用來處理 [] 的。為了執行 a[i, j] ,實際的 Python 呼叫為 a.__getitem__((i, j)) 。裡面的多維索引被封裝成了元組傳給了對應的特殊函式。但是 Python 內建的序列型別只是一維的,並不支援多維操作,sad。

Python 使用省略號來作為多維切片的簡寫,例如 x[i, ...] 等同於 x[i, :, :, :, ] 。此處 x 為四維陣列。

對切片賦值

可變的序列型別可以用切片進行直接操作。

l = list(range(10))
l # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
l[2:5] = [20, 30]
l # [0, 1, 20, 30, 5, 6, 7, 8, 9]
del l[5:7]
l # [0, 1, 20, 30, 5, 8, 9]
l[3::2] = [11, 22]
l # [0, 1, 20, 11, 5, 22, 9]
l[2:5] = 100
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: can only assign an iterable
l[2:5] = [100]
l # [0, 1, 100, 22, 9]複製程式碼

對切片進行賦值時,右側的物件必須得是可遍歷物件,即使只有一個元素。

使用 +* 來操作序列

程式設計師都期待序列型別支援 +* 。當使用 + 操作的時候,兩邊的物件必須是相同的序列型別,而且兩個物件都不能被改變,但是會產生一個相同型別的物件作為結果。

當把一個序列物件乘以一個整數的時候,一個新的序列會被建立,原序列不變。

l = [1, 2, 3]
l * 5 # [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
5 * `abc` # `abcdabcdabcdabcdabcd`複製程式碼

+* 都不會改變其運算元,但是會產生一個新的物件。

小建議:

當進行 a * n 的操作時,而 a 又是一個包含可變元素的序列時,結果可能會很有趣。

a = [[1]]
b = a * 3 # [[1], [1], [1]]
a[0][0] = 2
a # [[2]]
b # [[2], [2], [2]]複製程式碼

這裡面的傳遞是引用傳遞,所以進行修改時,會影響到很多。這方面切記!

建立一個列表的列表

有時我們需要建立一個包含巢狀列表的列表,實現這個的最好辦法就是列表解析。

# 例子2-12,包含三個長度為3列表的列表,可以用來表示一個一字棋
border = [[`_`] * 3 for i in range(3)] # 註釋1
print border # [[`_`, `_`, `_`], [`_`, `_`, `_`], [`_`, `_`, `_`]]
border[1][2] = `X` # 註釋2
print border # [[`_`, `_`, `_`], [`_`, `_`, `X`], [`_`, `_`, `_`]]複製程式碼
  1. 建立一個包含3個元素的列表,每個元素包含三個元素。注意檢視其結構
  2. 在第一行第二列出放一個標記,檢視結果

對比看來,例子2-13是一個錯誤的例子

# 例子2-13,同樣的例子,但是是錯的
weird_board = [[`_`] * 3] * 3 # 註釋1
print weird_board
weird_board[1][2] = `0` # 註釋2
print weird_board複製程式碼
  1. 最外層列表有三個指向相同引用的列表組成。不改變的時候,看起來沒毛病
  2. 在第一行第二列放置一個標記,可以發現每一行都指向了相同的元素

例子2-13其實相當於

row = [`_`] * 3
board = []
for i in range(3):
    board.append(row) # 註釋1複製程式碼
  1. 被新增到 board 裡面的列表都是同一個

而對應的,例子2-12和以下程式碼是等同的。

board = []
for i in range(3):
    row = [`_`] * 3 # 註釋1
    board.append(row)
print board # [[`_`, `_`, `_`], [`_`, `_`, `_`], [`_`, `_`, `_`]]
board[2][0] = `X`
print board # 註釋2
# [[`_`, `_`, `_`], [`_`, `_`, `_`], [`X`, `_`, `_`]]複製程式碼
  1. 每次迭代都新建立了一個列表,並新增到了 board 後面
  2. 只有對應的第二列改變了,如我們所期待的那樣

增廣的序列賦值

+=*= 可以和正常的運算非常不同。此處只討論 += ,其思想概念完全適用於 *= 。讓其工作的特殊函式為 __iadd__ 。如果這個特殊函式沒有被實現, Python 會呼叫 __add__ 作為備用。

a += b複製程式碼

如果 __iadd__ 被實現了就會被呼叫。對於可變序列(如 list, bytearray, array.array),a 會在原地被改變。而如果 __iadd__ 沒有被實現,那麼表示式 a += b 就和 a = a + b 完全一樣了。同樣的,*= 對應特殊函式 __imul__ ,一下是一些簡單例子。

l = [1, 2, 3]
id(l) # 4311953800 註釋1
l *= 2 
l # [1, 2, 3, 1, 2, 3]
id(l) # 4311953800 註釋2
t = (1, 2, 3)
id(t) # 4312681568 註釋3
t *= 2
id(t) # 4301348296 註釋4複製程式碼
  1. 初始列表的id
  2. 做了運算之後,l 還是其本身,只是在最後面新增上了新的元素
  3. 初始元組的id
  4. 做了運算之後,元組被改變

重複進行不可變序列的連結是效率很低的,因為除了將需要的元素新增到目標元素的後面之外,直譯器還得把得到的整個序列複製到新的記憶體中。

一個有趣的 += 賦值難題

# 例子2-14,一個謎題
t = (1, 2, [30, 40])
t[2] += [50, 60]複製程式碼

會發生什麼呢?t 是一個不可變的元組,是不能做修改的,而 t[2] 又是一個可變的列表。結果可能會讓人吃驚

# 例子2-15,例子2-14的輸出
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: `tuple` object does not support item assignment
print t # (1, 2, [30, 40, 50, 60])複製程式碼

也就是說 Python 一邊報錯,一邊輸出了正確的結果。(傲嬌的 Python )

這裡安利一下一個網站 Online Python Tutor ,在這裡可以看出程式執行期間具體都發生了什麼,有興趣就自己去看執行時吧。具體不介紹,但是誰用誰知道。

小建議:

值得指出的是,使用 t[2].extend([50, 60]) 就不會出錯。這裡只是指出 += 的怪異行為。

總結上面的例子:

  1. 把可變元素放在元組之中不是什麼好主意
  2. 增廣賦值操作並不是原子操作

list.sort 和內建的 sorted 函式

list.sort 函式會對 list 進行原地排序,函式的返回值為 None 來告知我們 list 本身已經被修改了,同時也不會產生新的 list 。這是 Python Api 的一個重要慣例:函式或者方法如果原地修改了一個物件,應該返回一個 None ,來讓人清楚的知道物件本身被改變了,以及沒有新的物件被建立。

與之形成對比的,Python 的內建函式 sorted 建立了一個新的 list 並將其返回。sorted 函式接受任何可遍歷物件作為輸入引數,包括不可變序列和生成器。無論輸入引數是什麼型別,sorted 函式總會建立一個新的 list ,並將其返回。

list.sortsorted 函式接收兩個可選的,只接受關鍵詞的引數

  • reverse 如果為 True ,元素會以逆序的形式返回,預設為 False
  • key 決定每個元素被排序時的排序依據。例如 key=str.lower 會進行大小寫無關的排序,key=len 會把字串按照長度排序。

具體的例子省略,但是沒有任何難度。

通過 bisect 函式來操作排序的序列

bisect(haystack, needle)會對 haystack 做二分查詢來搜尋 needle,這自然必須要求 haystack 是一個排序後的序列。

bisect 來搜尋

# 例子2-17,bisect 找到元素的插入位置
import bisect
import sys

HAYSTACK = [1, 4, 5, 6, 8, 12, 15, 20, 21, 23, 23, 26, 29, 30]
NEEDLES = [0, 1, 2, 5, 8, 10, 22, 23, 29, 30, 31]

ROW_FMT = `{0:2} @ {1:2}         {2}{0:<2}`

def demo(bisect_fn):
    for needle in reversed(NEEDLES):
        position = bisect_fn(HAYSTACK, needle) # 註釋1
        offset = position * `  |` # 註釋2
        print ROW_FMT.format(needle, position, offset) # 註釋3

if __name__ == `__main__`:

    if sys.argv[-1] == `left`: # 註釋4
        bisect_fn = bisect.bisect_left
    else:
        bisect_fn = bisect.bisect
    print(`DEMO:`, bisect_fn.__name__) # 註釋5
    print(`haystack ->`, ` `.join(`%2d` % n for n in HAYSTACK))
    demo(bisect_fn)複製程式碼
  1. 使用 bisect 函式來獲得插入點
  2. 根據偏移量來畫分割線
  3. 格式化輸出
  4. 通過命令列引數來選擇對應的 bisect 函式
  5. 在表頭列印函式名稱
(`DEMO:`, `bisect`)
(`haystack ->`, ` 1  4  5  6  8 12 15 20 21 23 23 26 29 30`)
31 @ 14           |  |  |  |  |  |  |  |  |  |  |  |  |  |31
30 @ 14           |  |  |  |  |  |  |  |  |  |  |  |  |  |30
29 @ 13           |  |  |  |  |  |  |  |  |  |  |  |  |29
23 @ 11           |  |  |  |  |  |  |  |  |  |  |23
22 @  9           |  |  |  |  |  |  |  |  |22
10 @  5           |  |  |  |  |10
 8 @  5           |  |  |  |  |8 
 5 @  3           |  |  |5 
 2 @  1           |2 
 1 @  1           |1 
 0 @  0         0複製程式碼

這張圖展示了 bisect 函式的具體工作過程。和函式 bisect_left 的比較就是,當遇到相同元素時,bisect (也就是 bisect_right),把元素插入了右邊,而 bisect_left 插到了左邊。例子2-18是一個簡單應用。

# 例子2-18,給定分數,輸入對應的成績分割槽
def grade(score, breakpoints=[60, 70, 80, 90], grades=`FDCBA`):
    i = bisect.bisect(breakpoints, score)
    return grades[i]

print [grade(score) for score in [33, 99, 77, 70, 89, 90, 100]]複製程式碼

bisect.insort 來插入

對序列排序是很耗時的操作,所以一旦一個序列已經排好序,我們希望後續的操作依舊可以保持排序狀態。

# 例子2-19,Insort函式保持排序狀態
import bisect
import random
SIZE = 7
random.seed(1729)
my_list = []
for i in range(SIZE):
    new_item = random.randrange(SIZE*2)
    bisect.insort(my_list, new_item)
    print(`%2d ->` % new_item, my_list)複製程式碼

輸出結果為:

(`13 ->`, [13])
(`12 ->`, [12, 13])
(` 5 ->`, [5, 12, 13])
(` 6 ->`, [5, 6, 12, 13])
(` 9 ->`, [5, 6, 9, 12, 13])
(` 2 ->`, [2, 5, 6, 9, 12, 13])
(` 4 ->`, [2, 4, 5, 6, 9, 12, 13])複製程式碼

也許有時 List 不是最好的選擇

list 是那麼的好用,以至於我們選擇使用 list 幾乎不需要做多餘的思考。但有時,例如我們需要儲存一個一千萬的浮點數列表,使用 array 是更高效的選擇,因為 array 並不是把 float 物件完全的存貯下來,而只是存下來了打包的位元組。如果我們需要不斷的新增或者刪除元素,這是 deque 是更好的選擇。如果你需要做大量的 in 操作,那麼 set 是更好的選擇。

Arrays

如果序列中只有數, array.array 是一個更高效的選擇。和 list 一樣,它也支援所有的可變序列的操作,包括 pop, insert, extend。它還支援快速載入和儲存的 frombytestofile 方法。

和 C 語言中的 array 一樣,建立 array 需要指明儲存型別。array(`b`) 表明每個元素都是一個位元組,被解釋為從 -128 到 127 的整數。對於體積很大的序列來說,這個很節省空間。同時 array 會檢查你的輸入,不會允許你把型別不對的元素放進去。

# 例子2-20,建立,儲存,載入一個大的陣列
from array import array # 註釋1
from random import random
floats = array(`d`, (random() for i in range(10**7))) # 註釋2
print floats[-1] # 註釋3
fp = open(`floats.bin`, `wb`)
floats.tofile(fp) # 註釋4
fp.close()
floats2 = array(`d`) # 註釋5
fp = open(`floats.bin`, `rb`)
floats2.fromfile(fp, 10**7) # 註釋6
fp.close()
print floats2[-1] # 註釋7
print floats2 == floats # 註釋8複製程式碼
  1. 建立一個陣列型別
  2. 從一個可遍歷物件中,建立一個雙精度浮點數的陣列(關鍵字為 d)。此處的可遍歷物件為生成器
  3. 檢視一下最後一個元素
  4. 把 array 儲存為二進位制檔案
  5. 建立一個雙精度浮點數的空陣列
  6. 從二進位制檔案中讀取一千萬個浮點數
  7. 檢查陣列中的最後一個數
  8. 比較兩個陣列

重點是,這些操作相當迅速。我自己曾經試著用 list 來運算元據,寫檔案然後讀檔案,相當慢。對於陣列來說 array.fromfile 只需要 0.1 秒的時間從二進位制檔案中來載入一千萬個雙精度浮點數的陣列,這個幾乎比從文字檔案中讀取要快 60 倍。同樣的,array.tofile 也比一個個往每一行中寫浮點數要快差不多 7 倍。更重要的是,儲存一千萬個浮點數的二進位制檔案只有差不多 80 M,而寫文字檔案則差不多要 180 M。

小建議:

在 Python 3.4 中,array 並沒有像 list.sort() 那樣的原地排序操作,如果需要的話,可以進行如下操作

a = array.array(`d`, sorted(a))複製程式碼

原始碼的github地址

原始碼
以上原始碼均來自於 fluent python 一書,僅為方便閱讀之用。如侵刪。

相關文章