zip() 函式的功能是將多個序列物件 (串列, 元組, 或字串) 中的對應元素配對為一群 tuple, 然後將其打包為一個可迭代的 zip 物件 (也是迭代器物件), 可用 for 迴圈或 next() 函式走訪其元素 (tuple), 也可以用 list(), tuple(), 或 dict() 轉換成串列, 元組, 或字典 (僅限兩個配對時), 語法如下 :
zipped=zip(序列1, 序列2, 序列3, .....)
zip() 的參數可以是任何序列型態資料, 例如串列, 元組, 或字串, 可以混搭配對, 例如串列與元組配對, 元組與字串配對等等. 當使用字串進行配對時, 字串會被拆成一個一個字元進行配對. 注意, 雖然將集合物件傳入 zip() 也會進行配對不會報錯, 但因為集合的元素是無序的, 配對的結果無法預測, 因此是無意義的.
另外, zip() 在配對時採用最短配對法 (shortest combination), 亦即當傳入之序列資料長度不同時, 配對會進行到最短的序列耗盡為止, 那些較長的序列元素無法被配對. Python 的內建模組 itertools 裡面有一個函式 zip_longest() 則是採用最長配對法, 亦即配對會持續進行到最長的序列元素耗盡為止.
注意, 由於 zip 物件是個迭代器, 一旦被走訪完畢或被轉換成 tuple, list, 與 dict, 其內容將被清空, 無法繼續做第二次走訪或其他轉換, 必須重新建立 zip 物件才行, 參考 :
參考書籍 :
- 精通 Python (碁峰, 2020) 第 7 章
- Python 入門邁向高手之路王者歸來 (深石, 2017) 8-11 節
- The Python 3 Standard Library by Example (Edison Weslly, 2017)
- Python最強入門邁向數據科學之路:王者歸來(全彩印刷第二版) 8-10 節
1. 兩個序列物件用 zip() 配對 :
zip() 最常用來將兩個序列物件進行配對, 傳回的 zip 物件可以用 tuple(), list(), 與 dict() 分別轉成元組, 串列, 與字典. 轉成字典時傳入 zip() 的第一參數會被當成 key, 第二參數會被當成 value, 例如 :
>>> company=['台積電', '聯發科', '台塑'] # 串列 1
>>> stock_id=['2330', '2454', '1301'] # 串列 2
>>> stocks=zip(company, stock_id) # 呼叫 zip() 配對串列元素
>>> type(stocks) # zip() 傳回 zip 物件
<class 'zip'>
>>> stocks
<zip object at 0x0000024D07D1B248>
>>> hasattr(stocks, '__iter__') # zip 物件是可迭代物件
True
>>> hasattr(stocks, '__next__') # zip 物件是迭代器物件
True
>>> for i in stocks: # 走訪 zip 物件內容
print(i) # zip 物件的元素都是 tuple
('台積電', '2330')
('聯發科', '2454')
('台塑', '1301')
>>> for i in stocks: # 再次走訪內容為空
print(i)
>>> stocks=zip(company, stock_id) # 重新建立 zip 物件
>>> list(stocks) # 將 zip 物件轉成串列
[('台積電', '2330'), ('聯發科', '2454'), ('台塑', '1301')]
>>> list(stocks) # 再次轉換內容為空
[]
>>> stocks=zip(company, stock_id) # 重新建立 zip 物件
>>> tuple(stocks) # 將 zip 物件轉成元組
(('台積電', '2330'), ('聯發科', '2454'), ('台塑', '1301'))
>>> tuple(stocks) # 再次轉換內容為空
()
>>> stocks=zip(company, stock_id) # 重新建立 zip 物件
>>> dict(stocks) # 將 zip 物件轉成字典
{'台積電': '2330', '聯發科': '2454', '台塑': '1301'}
可見 zip 物件的元素都是 tuple, 可以用 for 迴圈走訪, 也可以呼叫 dict(), tuple(), 或 dict() 將 zip 物件轉成串列, 元組, 或字典. 注意, 轉成字典時, 傳給 zip() 的第一個序列之元素會當作 key, 而第二個序列之元素則是做為 value.
由於 zip() 是將傳入序列的對應元素打包成 tuple 放入 zip 物件中, 因此在走訪 zip 物件時可用同步指定方式將 zip 物件元素解包 (unpacking) 給多個變數, 例如 :
>>> company=['台積電', '聯發科', '台塑'] # 串列 1
>>> stock_id=['2330', '2454', '1301'] # 串列 2
>>> stocks=zip(company, stock_id) # 呼叫 zip() 配對串列元素
>>> for c, s in stocks: # 利用同步指定方式將 zip 物件的元素 (tuple) 解包
print(c, s)
台積電 2330
聯發科 2454
台塑 1301
配對的序列可以是不同類型, 例如串列與元組配對, 或元組與字串配對等等, 只要是序列型態物件都可以進行混搭配對, 例如 :
>>> company=['台積電', '聯發科', '台塑'] # 序列 1 (串列)
>>> stock_id=('2330', '2454', '1301') # 序列 1 (元組)
>>> stocks=zip(company, stock_id) # 呼叫 zip() 配對元素
>>> list(stocks)
[('台積電', '2330'), ('聯發科', '2454'), ('台塑', '1301')]
>>> fruits=zip(('香蕉', '蘋果', '葡萄'), '123') # 元組與字串配對
>>> list(fruits)
[('香蕉', '1'), ('蘋果', '2'), ('葡萄', '3')]
可見字串參與配對時是拆開成字元當作元素去配對的.
以上是兩個串列長度相同時的情況, 如果要配對的兩個串列長度不同時, 則以長度較短者為準, 當配對完較短串列的最後元素後就會結束, 多出來的元素直接被忽略, 例如 :
>>> company=['台積電', '聯發科', '台塑', '台達電'] # 串列 1
>>> stock_id=['2330', '2454', '1301'] # 串列 2
>>> stocks=zip(company, stock_id) # 呼叫 zip() 配對串列元素
>>> list(stocks) # 轉成 list
[('台積電', '2330'), ('聯發科', '2454'), ('台塑', '1301')]
>>> stocks=zip(company, stock_id) # 呼叫 zip() 配對串列元素
>>> dict(stocks) # 轉成 dict
{'台積電': '2330', '聯發科': '2454', '台塑': '1301'}
>>> fruits=zip(('香蕉', '蘋果', '葡萄'), '1234') # 元組與字串配對
>>> list(fruits)
[('香蕉', '1'), ('蘋果', '2'), ('葡萄', '3')]
此例串列 company 比串列 stock_id 多了一個元素, 用 zip() 配對時最後一個元素無法找到對應元素直接被忽略.
由於字串參與配對時是拆成字元去對應, 配對後若轉成 list 或 tuple 較無問題, 但轉成字典時由於 key 不能重複, 後面配對成功的 key:value 會取代前面的相同 key 的鍵值對, 例如 :
>>> zip1=zip('Hello', 'World')
>>> print(zip1)
<zip object at 0x0000024D07CF1848>
>>> type(zip1)
<class 'zip'>
>>> list(zip1) # 轉成串列
[('H', 'W'), ('e', 'o'), ('l', 'r'), ('l', 'l'), ('o', 'd')]
>>> list(zip1) # 再次轉成串列內容為空
[]
>>> zip1=zip('Hello', 'World') # 重新建立 zip 物件
>>> tuple(zip1) # 轉成元組
(('H', 'W'), ('e', 'o'), ('l', 'r'), ('l', 'l'), ('o', 'd'))
>>> tuple(zip1) # 再次轉成元組內容為空
()
>>> zip1=zip('Hello', 'World') # 重新建立 zip 物件
>>> dict(zip1) # 轉成字典
{'H': 'W', 'e': 'o', 'l': 'l', 'o': 'd'} # 'l':'r' 的配對因為 key 重複被取消
此處因為兩個字串長度相同, 故轉成元組與串列時兩個字串的對應字元都能恰好配對; 但轉成字典時則少了 'l':'r' 的配對, 這是因為進行到 'l':'l' 配對時發現 key='l' 在前面 'l':'r' 配對時已用過此鍵, 故將前面的 'l':'r' 配對刪除, 用較後面的配對取代, 如下圖所示 :
下面是兩個不同長度字串的配對 :
>>> zip2=zip('Hello', 'Tony')
>>> list(zip2)
[('H', 'T'), ('e', 'o'), ('l', 'n'), ('l', 'y')]
>>> zip2=zip('Hello', 'Tony')
>>> tuple(zip2)
(('H', 'T'), ('e', 'o'), ('l', 'n'), ('l', 'y'))
>>> zip2=zip('Hello', 'Tony')
>>> dict(zip2)
{'H': 'T', 'e': 'o', 'l': 'y'}
此例前兩組字元配對 key 不同所以沒甚麼問題, 但第三組字元配對 'l':'n', 與第四組配對 'l':'y' 的 key 重複, 前一對 'l':'n' 會被後一對 'l':'y' 取代掉. 如下圖所示 :
從上面的測試可以看出, 其實 zip() 可以拿來建立字典, 只要將鍵序列傳給 zip() 當第一參數, 而值序列當作第二參數即可, 例如 :
>>> keys=['apple', 'banana', 'grape'] # 鍵串列
>>> values=['蘋果', '香蕉', '葡萄'] # 值串列
>>> fruits=dict(zip(keys, values)) # k:v 用 zip() 配對後轉成 dict
>>> fruits
{'apple': '蘋果', 'banana': '香蕉', 'grape': '葡萄'}
2. 三個或以上序列物件用 zip() 配對 :
多於兩個序列物件用 zip() 函式進行配對方式與上面兩個配對唯一的不同是不能用 dict() 將其轉成字典, 因為每一組配對都有三個以上的元素, 而 dict() 只能處理兩個元素 (鍵值關係), 例如 :
>>> company=['台積電', '聯發科', '台塑'] # 串列 1
>>> stock_id=['2330', '2454', '1301'] # 串列 2
>>> industry=['半導體', '半導體', '塑膠'] # 串列 3
>>> stocks=zip(company, stock_id, industry) # 用 zip() 進行元素配對
>>> list(stocks) # 轉成串列
[('台積電', '2330', '半導體'), ('聯發科', '2454', '半導體'), ('台塑', '1301', '塑膠')]
>>> stocks=zip(company, stock_id, industry) # 用 zip() 進行元素配對
>>> tuple(stocks) # 轉成元組
(('台積電', '2330', '半導體'), ('聯發科', '2454', '半導體'), ('台塑', '1301', '塑膠'))
>>> stocks=zip(company, stock_id, industry) # 用 zip() 進行元素配對
>>> dict(stocks) # 轉成字典
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
ValueError: dictionary update sequence element #0 has length 3; 2 is required
可見由於配對後的 tuple 有三個元素, 呼叫 dict() 轉成字典時會出現 ValueError (因為不知要如何處理鍵值關係), 只能轉成元組或串列.
3. 用 zip(*序列) 解配對 :
如果將一個 zip 物件前面冠一個 * 傳入 zip() 函式則會將其解配對, 傳回值為一個解配對後的 zip 物件, 同樣可如上述那樣使用 for 迴圈或 next() 走訪其元素, 也可以傳入 tuple() 或 list() 等函式轉成元組或串列, 語法如下 :
unzipped=zip(*zipped)
注意, 此處解配對後得到的 zip 物件 unzipped 同樣只能走訪或轉換一次, 且與解配對前的 zip 物件 zipped 連動, 亦即 unzipped 被走訪完或轉換過後不僅自己被清空, 連 zipped 也會被清空. 因此, 如果要重新走訪會轉換必須重新配對, 而不是僅僅重新解配對而已.
例如 :
>>> company=['台積電', '聯發科', '台塑'] # 串列 1
>>> stock_id=['2330', '2454', '1301'] # 串列 2
>>> industry=['半導體', '半導體', '塑膠'] # 串列 3
>>> stocks=zip(company, stock_id, industry) # 用 zip() 進行元素配對
>>> unzipped=zip(*stocks) # 解配對
>>> type(unzipped) # 傳回的也是 zip 物件
<class 'zip'>
>>> tuple(unzipped) # 轉成 tuple
(('台積電', '聯發科', '台塑'), ('2330', '2454', '1301'), ('半導體', '半導體', '塑膠'))
>>> tuple(stocks) # 原來配對的 zip 物件也空了
()
>>> tuple(unzipped) # zip 物件只能走訪或轉換一次
() # 再次轉換內容為空
>>> unzipped=zip(*stocks) # 僅重新解配對不行 (因 stocks 也空了)
>>> list(unzipped) # 內容仍是空
[]
>>> stocks=zip(company, stock_id, industry) # 須重新配對
>>> unzipped=zip(*stocks) # 再解配
>>> list(unzipped) # 轉成串列
[('台積電', '聯發科', '台塑'), ('2330', '2454', '1301'), ('半導體', '半導體', '塑膠')]
>>> list(stocks) # 原來配對的 zip 物件也空了
[]
可見解配對其實就是對應元素的重新配對而已, 走訪被解配對的 zip 檔時, 其配對的 zip 檔也會同時被清空, 所以想再次解配對 zip 元件須重新建立其源頭的 zip 物件才行.
其實 zip(*) 的解配對語法並非只能用在 zip 物件, 也可以傳入雙層的序列物件, 例如 :
- 元組的串列
- 串列的元組
- 串列的串列
- 元組的元組
注意, 與上面不同的是, 由於被解配對的源頭不是 zip 物件 (而是雙層的序列物件), 因此即使走訪解配對後的 zip 物件, 被解配對的雙層序列物件並不會被清空, 例如 :
>>> pair1=[('台積電', '聯發科', '台塑'), ('2330', '2454', '1301'), ('半導體', '半導體', '塑膠')]
>>> pair2=zip(*pair1) # 解配對雙層序列物件
>>> type(pair2) # 傳回 zip 物件
<class 'zip'>
>>> list(pair2) # 將解配對後的 zip 物件轉成 list
[('台積電', '2330', '半導體'), ('聯發科', '2454', '半導體'), ('台塑', '1301', '塑膠')]
>>> list(pair2) # zip 物件只能走訪或轉換一次
[]
>>> pair1 # 被解配對的雙層序列物件不會被清空
[('台積電', '聯發科', '台塑'), ('2330', '2454', '1301'), ('半導體', '半導體', '塑膠')]
4. 用 zip_longest() 做最長配對 :
以上測試的 zip() 函式在替序列元素進行配對時採用最短配對法, 亦即當最短的序列元素用光時配對就會停止. Python 內建模組 itertools 內的 zip_longest() 函式則是採用最長配對法, 配對會一直進行直到最長的那個序列之元素用光才停止, 那些較短的序列會用 None 來填補不足的元素. 其語法如下 :
from itertools import zip_longest
zipped=zip_longest(序列1, 序列2, 序列3, .....)
zip_longest() 會傳回一個 zip_longest 物件, 它與 zip 物件一樣是可迭代物件與迭代器物件, 可用 for 迴圈或 next() 函式走訪其元素 (tuple). 不過最長配對的實際應用機會似乎較少.
例如 :
>>> from itertools import zip_longest
>>> s1=['a', 'b', 'c']
>>> s2=('甲', '乙', '丙', '丁', '戊', '己')
>>> s3='12345'
>>> zipped1=zip(s1, s2, s3) # 用 zip() 做最短配對
>>> list(zipped1)
[('a', '甲', '1'), ('b', '乙', '2'), ('c', '丙', '3')]
>>> zipped2=zip_longest(s1, s2, s3) # 用 zip_longest() 做最長配對
>>> type(zipped2) # zip_longest() 傳回一個 zip_longest 物件
<class 'itertools.zip_longest'>
>>> hasattr(zipped2, '__iter__') # zip_longest 物件為可迭代物件
True
>>> hasattr(zipped2, '__next__') # zip_longest 物件為迭代器物件
True
>>> list(zipped2) # 轉成串列
[('a', '甲', '1'), ('b', '乙', '2'), ('c', '丙', '3'), (None, '丁', '4'), (None, '戊', '5'), (None, '己', None)]
>>> zipped2=zip_longest(s1, s2, s3) # 重新建立 zip_longest 物件
>>> for i, j, k in zipped2: # 用 for 迴圈走訪並解包 tuple
print(i, j, k)
a 甲 1
b 乙 2
c 丙 3
None 丁 4
None 戊 5
None 己 None
可見較短的序列因為元素耗盡而以 None 填補進行配對.
zip_longest() 同樣也支援用 * 解配對的用法, 也是傳回一個 zip_longest 物件 :
unzipped=zip_longest(*zipped)
例如 :
>>> from itertools import zip_longest
>>> s1=['a', 'b', 'c']
>>> s2=('甲', '乙', '丙', '丁', '戊', '己')
>>> s3='12345'
>>> zipped2=zip_longest(s1, s2, s3) # 用 zip_longest() 做最長配對
>>> unzipped2=zip_longest(*zipped2) # 解配對
>>> type(unzipped2)
<class 'itertools.zip_longest'> # 同樣傳回 zip_longest 物件
>>> list(unzipped2) # 轉成串列
[('a', 'b', 'c', None, None, None), ('甲', '乙', '丙', '丁', '戊', '己'), ('1', '2', '3', '4', '5', None)]
可見解配對後得到的 tuple 長度都被 None 填補為相同長度了.
參考 :
沒有留言 :
張貼留言