2021年11月1日 星期一

Python 學習筆記 : 內建函式 zip() 的用法

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 物件才行, 參考 :


參考書籍 :

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   
NoneNone     

可見較短的序列因為元素耗盡而以 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 填補為相同長度了. 

參考 :


沒有留言 :