對 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('\n')[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'], ['_', '_', '_']]複製程式碼
- 建立一個包含3個元素的列表,每個元素包含三個元素。注意檢視其結構
- 在第一行第二列出放一個標記,檢視結果
對比看來,例子2-13是一個錯誤的例子
# 例子2-13,同樣的例子,但是是錯的
weird_board = [['_'] * 3] * 3 # 註釋1
print weird_board
weird_board[1][2] = '0' # 註釋2
print weird_board複製程式碼
- 最外層列表有三個指向相同引用的列表組成。不改變的時候,看起來沒毛病
- 在第一行第二列放置一個標記,可以發現每一行都指向了相同的元素
例子2-13其實相當於
row = ['_'] * 3
board = []
for i in range(3):
board.append(row) # 註釋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', '_', '_']]複製程式碼
- 每次迭代都新建立了一個列表,並新增到了 board 後面
- 只有對應的第二列改變了,如我們所期待的那樣
增廣的序列賦值
+=
和 *=
可以和正常的運算非常不同。此處只討論 +=
,其思想概念完全適用於 *=
。讓其工作的特殊函式為 __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複製程式碼
- 初始列表的id
- 做了運算之後,l 還是其本身,只是在最後面新增上了新的元素
- 初始元組的id
- 做了運算之後,元組被改變
重複進行不可變序列的連結是效率很低的,因為除了將需要的元素新增到目標元素的後面之外,直譯器還得把得到的整個序列複製到新的記憶體中。
一個有趣的 +=
賦值難題
# 例子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])
就不會出錯。這裡只是指出+=
的怪異行為。
總結上面的例子:
- 把可變元素放在元組之中不是什麼好主意
- 增廣賦值操作並不是原子操作
list.sort 和內建的 sorted 函式
list.sort
函式會對 list 進行原地排序,函式的返回值為 None
來告知我們 list 本身已經被修改了,同時也不會產生新的 list 。這是 Python Api 的一個重要慣例:函式或者方法如果原地修改了一個物件,應該返回一個 None ,來讓人清楚的知道物件本身被改變了,以及沒有新的物件被建立。
與之形成對比的,Python 的內建函式 sorted
建立了一個新的 list 並將其返回。sorted
函式接受任何可遍歷物件作為輸入引數,包括不可變序列和生成器。無論輸入引數是什麼型別,sorted
函式總會建立一個新的 list ,並將其返回。
list.sort
和 sorted
函式接收兩個可選的,只接受關鍵詞的引數
- 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)複製程式碼
- 使用
bisect
函式來獲得插入點 - 根據偏移量來畫分割線
- 格式化輸出
- 通過命令列引數來選擇對應的
bisect
函式 - 在表頭列印函式名稱
('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。它還支援快速載入和儲存的 frombytes
和 tofile
方法。
和 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複製程式碼
- 建立一個陣列型別
- 從一個可遍歷物件中,建立一個雙精度浮點數的陣列(關鍵字為 d)。此處的可遍歷物件為生成器
- 檢視一下最後一個元素
- 把 array 儲存為二進位制檔案
- 建立一個雙精度浮點數的空陣列
- 從二進位制檔案中讀取一千萬個浮點數
- 檢查陣列中的最後一個數
- 比較兩個陣列
重點是,這些操作相當迅速。我自己曾經試著用 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 一書,僅為方便閱讀之用。如侵刪。