zip
函數是Python的內置函數,在拙做《跟老齊學Python:輕鬆入門》有必定的介紹,可是,考慮到那本書屬於Python的入門讀物,並無講太深。可是,並不意味着這個函數不能應用的很深刻,特別是結合迭代器來理解此函數,會讓人有豁然開朗的感受。同時,可以巧妙地解決某些問題。html
本文試圖對zip
進行深刻探討,同時兼顧對迭代器的理解。python
看下面的操做。數組
>>> list(zip([1, 2, 3], ['a', 'b', 'c']))
[(1, 'a'), (2, 'b'), (3, 'c')]
複製代碼
[1, 2, 3]
和 ['a', 'b', 'c']
是列表,全部列表都是可迭代的。這意味着能夠一次返回一個元素。bash
zip函數最終獲得一個zip對象——一個迭代器對象。而這個迭代器對象是由若干個元組組成的。函數
那麼這些元組是如何生成的呢?測試
對照上面的代碼,從開始算起。按照Python中的技術習慣,開始的那個元組是用0來計數的,即第0個。ui
[1,2,3]
)當前指針所指的值,剛開始讀取,那就是1;['a', 'b', 'c']
)當前指針所指的值,也是剛剛開始讀取,應該是a;因而組成了zip對象的第0個元組(1, 'a')
。(2, 'b')
。請注意上面的敘述,若是把元組中的組成對象來源歸納一句話,那就是:元組中的第i個元素就來自於第i個參數中指針在當前所指的元素對象——請細細品味這句話的含義,後面有大用途。編碼
對於zip
函數,上面的過程,貌似「壓縮」同樣,那麼,是否有反過程——解壓縮。例如從上面示例的結果中分別恢復出來原來的兩個對象。lua
有。通常認爲是這這麼作:spa
>>> result = zip([1, 2, 3], ["a", "b", "c"])
>>> c, v = zip(*result)
>>> print(c, v)
(1, 2, 3) ('a', 'b', 'c')
複製代碼
這是什麼原理。
result
應用的是一個迭代器對象,不過爲了可以顯示的明白,也能夠理解爲是[(1, 'a'), (2, 'b'), (3, 'c')]
。接下來使用了zip(*result)
,這裏的符號*
的做用是收集參數,在函數的參數中,有對此詳細闡述(請參閱《跟老齊學Python:輕鬆入門》)。
>>> def foo(*a): print(a)
...
>>> lst = [(1,2), (3,4)]
>>> foo(*lst)
((1, 2), (3, 4))
>>> foo((1,2), (3,4))
((1, 2), (3, 4))
複製代碼
仿照這個示例,就能明晰下面兩個操做是等效的。
>>> lst = [(1, 'a'), (2, 'b'), (3, 'c')]
>>> zip(*lst)
<zip object at 0x104c8fb48>
>>> zip((1, 'a'), (2, 'b'), (3, 'c'))
<zip object at 0x104f27308>
複製代碼
從返回的對象內存編碼也能夠看出,兩個是一樣的對象。
既然如此,咱們就能夠經過理解zip((1, 'a'), (2, 'b'), (3, 'c'))
的結果產生過程來理解zip(*lst)
了。而前者生成結果的過程前面已經闡述過了,此處再也不贅述。
原來,所謂的「解壓縮」和「壓縮」,計算的方法是同樣的。豁然開朗。
除了zip
函數,還有一個內置函數iter
,它以可迭代對象爲參數,會返回迭代器對象。
>>> iter([1, 2, 3, 4])
<list_iterator object at 0x7fa80af0d898>
複製代碼
本質上,iter
函數調用參數的每一個元素,而後藉助於__next__
函數返回迭代器對象,並把結果集成到一個元組中。
內置函數map
是另一個返回迭代器對象的函數,它以只有一個參數的函數對象爲參數,這個函數每次從可迭代對象中取一個元素。
>>> list(map(len, ['abc', 'de', 'fghi']))
[3, 2, 4]
複製代碼
map
函數的執行原理是:用__iter__
函數調用第二個參數,並用__next__
函數返回執行結果。在上面的例子中,len
函數要調用後面的列表中的每一個元素,並返回一個迭代器對象。
既然迭代器是可迭代的,就能夠把zip
返回的迭代器對象用到map
函數的參數中了。例如,用下面的方式計算兩個列表中對應元素的和。
>>> list(map(sum, zip([1, 2, 3], [4, 5, 6])))
[5, 7, 9]
複製代碼
迭代器對象有兩個主要功效,一是節省內存,而是提升執行效率。
有一個列表,由一些正整數組成,如[1, 2, 3, 4, 5, 6]
,寫一個函數,函數的一個參數n
表示要將列表中幾個元素劃爲一組,假設n=2,則將兩個元素爲一組,最終返回[(1, 2), (3, 4), (5, 6)]
。
若是用簡單的方式,能夠這樣寫此函數:
def naive_grouper(inputs, n):
num_groups = len(inputs) // n
return [tuple(inputs[i*n:(i+1)*n]) for i in range(num_groups)]
複製代碼
測試一下,此函數可以如咱們所願那樣工做。
>>> nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> naive_grouper(nums, 2)
[(1, 2), (3, 4), (5, 6), (7, 8), (9, 10)]
複製代碼
可是,在上面的測試中,所傳入的列表元素個數是比較小的,若是列表元素個數不少,好比有100萬個。這就須要有比較大的內存了,不然沒法執行運算。
可是,若是這樣執行此程序:
def naive_grouper(inputs, n):
num_groups = len(inputs) // n
return [tuple(inputs[i*n:(i+1)*n]) for i in range(num_groups)]
for _ in naive_grouper(range(100000000), 10):
pass
複製代碼
把上面的程序保存爲文件naive.py
。能夠用下面的指令,測量運行程序時所佔用的內存空間和耗費的時長。注意,要確保你本地機器的內存至少5G。
$ time -f "Memory used (kB): %M\nUser time (seconds): %U" python3 naive.py
Memory used (kB): 4551872
User time (seconds): 11.04
複製代碼
注意:Ubuntu系統中,你可能要執行 /usr/bin/time
。
把列表或者元素傳入naïve_grouper
函數,須要計算機提供4.5GB的內存空間,才能執行range(100000000)
的循環。
若是採用迭代器對象,就會有很大變化了。
def better_grouper(inputs, n):
iters = [iter(inputs)] * n
return zip(*iters)
複製代碼
這個簡短的函數中,內涵仍是很豐富的。因此咱們要逐行解釋。
表達式[iters(inputs)] * n
建立一個迭代器對象,它包含了n個一樣的列表對象。
下面以n=2
爲例說明。
>>> nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> iters = [iter(nums)] * 2
>>> list(id(itr) for itr in iters) # 內存地址是同樣的
[139949748267160, 139949748267160]
複製代碼
在iters
中的兩個迭代器對象是同一個對象——認識到這一點很是重要。
結合前面對zip
的理解,zip(*iters)
和zip(iter(nums), iter(nums))
是同樣的。爲了可以以更直觀的方式進行說明,就能夠認爲是zip((1, 2, 3, 4, 5, 6, 7, 8, 9, 10), (1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
。按照前文所述的zip
工做流程,其計算過程以下:
(1, 2)
。>>> nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> list(better_grouper(nums, 2))
[(1, 2), (3, 4), (5, 6), (7, 8), (9, 10)]
複製代碼
上面的函數better_grouper()
的優勢在於:
len()
,可以以任何可迭代對象爲參數把上述流程保存爲文件better.py
。
def better_grouper(inputs, n):
iters = [iter(inputs)] * n
return zip(*iters)
for _ in better_grouper(range(100000000), 10):
pass
複製代碼
而後使用 time
在終端執行。
$ time -f "Memory used (kB): %M\nUser time (seconds): %U" python3 better.py
Memory used (kB): 7224
User time (seconds): 2.48
複製代碼
對比前面執行 naive.py
,不論在內存仍是執行時間上,都表現很是優秀。
對於上面的better_grouper
函數,深刻分析一下,發現它還有問題。它只能分割可以被列表長度整除的列表中的數字,若是不能整除的話,就會有一些元素被捨棄。例如:
>>> nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> list(better_grouper(nums, 4))
[(1, 2, 3, 4), (5, 6, 7, 8)]
複製代碼
若是要4個元素一組,就會有9和10不能分組了。之因此,仍是由於zip
函數。
>>> list(zip([1, 2, 3], ['a', 'b', 'c', 'd']))
[(1, 'a'), (2, 'b'), (3, 'c')]
複製代碼
最終獲得的zip對象中的元組數量,是參數中長度最小的對象決定。
若是你感受這樣作不爽,可使用itertools.zip_longest()
,這個函數是以最長的參數爲基準,若是有不足的,默認用None
填充,固然也能夠經過參數fillvalue
指定填充對象。
>>> import itertools
>>> x = [1, 2, 3]
>>> y = ["a", "b", "c", "d"]
>>> list(itertools.zip_longest(x, y))
[(1, 'a'), (2, 'b'), (3, 'c'), (None, 'd')]
複製代碼
那麼,就能夠將better_grouper
函數中的zip
用zip_longest()
替代了。
import itertools as it
def grouper(inputs, n, fillvalue=None):
iters = [iter(inputs)] * n
return it.zip_longest(*iters, fillvalue=fillvalue)
複製代碼
再跑一下,就是這樣的結果了。
>>> nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> print(list(grouper(nums, 4)))
[(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, None, None)]
複製代碼