Saturday 3 September 2022

Python 函式 zip() 教學:同時迭代多個 list,學習刷題與資料分析技巧

 zip 在英文是拉鍊的意思,而 Python 的內建函式 zip() 就是取名自拉鍊的形象,它可以同時迭代多個 list、每次分別從各個 list 中取一個元素配成同一組,彷彿拉鍊齒一組一組整齊對應的樣子。

這則筆記中,好豪將跟你分享 zip() 函式有哪些好用的地方,我會先簡短教學 zip() 函式的功能,並把重點放在能讓你快速活用的三項實戰案例:

  • 矩陣轉置:用 Python 解 LeetCode 演算法題目常用技巧
  • dict 反轉:NLP 資料分析建立詞庫
  • 同時迭代多個長短不一的 listzip() 的進階用法
Python zip() 每次從各個引數 list 中各取一個元素配成同一組,彷彿拉鍊齒一個個整齊對應的樣子(Source: Unsplash

快速學習 zip 函式

zip():從每個引數中的可迭代物件,組合各個可迭代物件的元素,產生一個迭代器
Python3 官方文件

在 Python3,zip() 會回傳一個迭代器(iterator),而這個迭代器的第 i 個元素,就是每個傳入引數的第 i 個內容所組合成的 tuplezip() 的引數需要是可迭代物件,包括常用的 list、set、dict、或者 str 等等。請看以下範例:

# 基本 zip() 教學範例
>>> x = ['a', 'b', 'c']
>>> y = [1, 2, 3]
>>> zipped = zip(x, y)
>>> type(zipped) # 回傳的是一個 'zip' 物件,它是可迭代的
<class 'zip'>
>>> zipped
<zip object at 0x108e8bc80>
## 用 loop 遍歷 zip 物件內容
>>> for i in zip(x, y):
... print(i)
('a', 1)
('b', 2)
('c', 3)
# 也可用 list() 或 set() 將迭代器轉換成其他資料型態
>>> list(zip(x, y))
[('a', 1), ('b', 2), ('c', 3)]
>>> set(zip(x, y))
{('c', 3), ('b', 2), ('a', 1)}
## zip() 可以接受任意多個引數
## 函式引數需要是可迭代物件,因此也包括 str, set, dict 等等
>>> x = "abc"
>>> y = {2021, 1, 29}
>>> z = {'first': 0.5, 'second': 0.6, 'third': 0.7}
>>> for i in zip(x, y, z):
... print(i)
('a', 1, 'first')
('b', 29, 'second')
('c', 2021, 'third')
## 在這個例子中,有兩項小提醒
## 1. 如果你 zip 遍歷內容是 set,要注意它沒有排序的特性
## 本例 set 的 print 的順序就跟宣告內容順序不同
## 2. 當你直接遍歷 dict 物件的時候,只會遍歷 key、而不是 value 喔
## 如果需要遍歷 dict 的 value,可以使用 .values() 函式
>>> for i in zip(x, y, z.values()):
... print(i)
('a', 1, 0.5)
('b', 29, 0.6)
('c', 2021, 0.7)

究竟使用 zip() 函式有什麼好處呢?我們直接從以下三個實戰案例來學習。



矩陣轉置

給定一個 3*2 的矩陣(在 Python 內的資料結構可以用 3 個 list、各有 2 個元素來表示),要如何轉置成 2*3 的矩陣呢?

矩陣轉置範例(Source: 維基百科

矩陣轉置只需要使用 zip(*matrix) 這個簡單技巧就能完成:

# zip() 矩陣轉置範例
>>> matrix = [
... [1, 2],
... [3, 4],
... [5, 6]
... ]
>>> for i in zip(*matrix):
... print(i)
(1, 3, 5)
(2, 4, 6)
## 如果轉置後希望維持原本 list of lists 的結構
>>> [list(row) for row in zip(*matrix)]
[
[1, 3, 5],
[2, 4, 6]
]

原理說明:用  符號解包

呼叫函式的時候,如果在可迭代物件的引數前面加上 * 符號,可以進行解包(unpack):將可迭代物件內的每個內容物各自成為引數。

  • 如果呼叫 func(my_list),函式只會收到一個引數
  • 如果呼叫 func(*my_list),該 list 長度多長、函式就收到幾個引數

在轉置矩陣的範例中,矩陣解包後,每一列各自成為一個引數傳入 zip() 了。

## 沿用上方轉置矩陣範例
>>> matrix = [
... [1, 2],
... [3, 4],
... [5, 6]
... ]
>>> for i in zip(*matrix):
... print(i)
(1, 3, 5)
(2, 4, 6)
## 使用 `*` 解包,等同於:
>>> for i in zip([1, 2], [3, 4], [5, 6]):
... print(i)
(1, 3, 5)
(2, 4, 6)

LeetCode 實戰

此範例來自 LeetCode 477 (難度 Medium),筆者在此只探討用到 zip() 解題的部分。

Hamming Distance 是要計算兩個二進位制的數總共有幾個位元(bit)不一樣,而本題是需要你計算多個數的 Hamming Distance 總和。例如,要計算 1、7、與 10 三個數在二進位制總共有幾個位元不同。

如何一個個位元比較呢?用 zip() 處理二進位制字串就很好解決了!

## 使用 f-string 的 'b' 關鍵字
## 將整數轉成二進位制字串
>>> arr = [1, 7, 10]
>>> arr = [f"{i:04b}" for i in arr]
## 現在 arr 可以被看成是 3*4 的矩陣
>>> arr
['0001', '0111', '1010']
## 我們用 zip(*arr) 技巧將它轉置
## 成為 4*3 的矩陣
>>> for i in zip(*arr):
... print(i)
('0', '0', '1')
('0', '1', '0')
('0', '1', '1')
('1', '1', '0')

zip() 幫你把各個數的同一個位元都整齊放在同一列了,接下來你只要專注想出每一列要怎麼加總位數差異就可以了!

如果這題解完你還覺得練習不夠,和你分享 LeetCode 這題 也適合用 zip() 解題。

dict 反轉

處理資料的時候,zip() 可以用來將 dict 的 key 與 value 關係反轉。

# dict 反轉範例
>>> my_dict = {'a': 1, 'b': 2, 'c': 3}
## 快速複習用來列出 dict 內容的函式
>>> my_dict.keys()
dict_keys(['a', 'b', 'c'])
>>> my_dict.values()
dict_values([1, 2, 3])
>>> my_dict.items()
dict_items([('a', 1), ('b', 2), ('c', 3)])
# dict 反轉,兩種寫法
## 1. dict comprehension
>>> {value: key for key, value in my_dict.items()}
{1: 'a', 2: 'b', 3: 'c'}
## 2. 使用 zip()
>>> dict(zip(my_dict.values(), my_dict.keys()))
{1: 'a', 2: 'b', 3: 'c'}

上面提了兩種方式做到 dict 的 key 與 value 反轉,讀者可以依照自己的寫程式風格選擇要用哪種。

運用場景:NLP 建立詞庫

在做 NLP 文字分析時,會需要建立詞庫,文字要編成索引編號(index)才能輸入到機器學習模型,又因為模型輸出也是索引編號,所以我們還需要將索引編號轉換回來原本的文字。

要同時做到 文字 -> index 以及 index -> 文字,基本作法就是創造兩個 key 與 value 相反的 dict。

# NLP 詞庫建立範例
>>> sentence = "My name is haohao. This is my blog."
>>> sentence_list = [w.lower().strip('.') for w in sentence.split()]
>>> sentence_list
['my', 'name', 'is', 'haohao', 'this', 'is', 'my', 'blog']
>>> set(sentence_list) # 用 set 去除重複字詞
{'is', 'name', 'this', 'haohao', 'my', 'blog'}
## 先建立 `文字 -> index` 的詞庫
>>> word2index = {w: i for i, w in enumerate(set(sentence_list))}
>>> word2index
{'is': 0, 'name': 1, 'this': 2, 'haohao': 3, 'my': 4, 'blog': 5}
## 再用 zip() 反轉 dict,建立 `index -> 文字` 的對應關係
>>> index2word = dict(zip(word2index.values(), word2index.keys()))
>>> index2word
{0: 'is', 1: 'name', 2: 'this', 3: 'haohao', 4: 'my', 5: 'blog'}
## 可以任意讓資料在 `文字` 與 `index` 之間自由切換囉!
>>> sentence_list
['my', 'name', 'is', 'haohao', 'this', 'is', 'my', 'blog']
>>> sentence_index_list = [word2index[w] for w in sentence_list]
>>> sentence_index_list
[4, 1, 0, 3, 2, 0, 4, 5]
>>> [index2word[i] for i in sentence_index_list]
['my', 'name', 'is', 'haohao', 'this', 'is', 'my', 'blog']

NLP 詞庫建立,通常還需要考慮詞頻等問題,可以參考 Udacity 課程教學的簡單詞庫寫法(Github)。如果對入門 NLP 與深度學習有更多興趣,推薦你閱讀筆者好豪 學習這門 Udacity 深度學習 Pytorch 課程的心得,一起來上課!

同時迭代多個長短不一的 list

請注意:如果你傳給 zip() 的多個可迭代物件長度不一樣,zip() 不會回報 error,而是走訪完最短的可迭代物件就結束。

如果你沒察覺到傳入的多個 list 長短不一,長度較長的 list 數值就會在走訪時被遺漏

# 範例:多個 list 長短不一,又想用 zip() 同時走訪
>>> list_a = [1, 2]
>>> list_b = ['a', 'b', 'c', 'd', 'e']
>>> list_c = [0.5, 0.7, 0.9]
>>> for i in zip(list_a, list_b, list_c):
... print(i)
(1, 'a', 0.5)
(2, 'b', 0.7)
## ^ 明明最長的 list 有 5 個元素
## 使用 zip() 之後只剩下走訪 2 個元素!
## 如果 zip() 引數之中混入一個長度為 0 的 list
## 就不會走訪任何內容囉
## 而且不會出現 error
>>> for i in zip([], list_a, list_b, list_c):
... print(i)
>>> len(list(zip([], list_a, list_b, list_c)))
0

如果你需要以最長的 list 為準、用 zip() 同時走訪多個 list,可以把 zip() 替換成 itertools 內建函式庫 裡的 zip_longest() 函式:

>>> from itertools import zip_longest
>>> for i in zip_longest(list_a, list_b, list_c):
... print(i)
(1, 'a', 0.5)
(2, 'b', 0.7)
(None, 'c', 0.9)
(None, 'd', None)
(None, 'e', None)
## 上例中,較短的 list 先被走訪完畢,之後都預設填補 None
## 設定 zip_longest() 的 fillvalue 引數可以選擇填補其他內容
>>> for i in zip_longest(list_a, list_b, list_c, fillvalue="NO_VALUE"):
... print(i)
(1, 'a', 0.5)
(2, 'b', 0.7)
('NO_VALUE', 'c', 0.9)
('NO_VALUE', 'd', 'NO_VALUE')
('NO_VALUE', 'e', 'NO_VALUE')
## 要用 zip() 同時走訪長短不一的 list、並且要把最長的 list 走訪完
## itertools 函式庫還有其他工具可用
## 在此只用範例簡單展示其中兩個:
## - cycle(): 較短的 list 被走訪完後,會從頭再走訪、不斷重複
## - repeat(): 單純地重複某個元素
>>> from itertools import cycle, repeat
>>> for i in zip(cycle(list_a), list_b, cycle(list_c), repeat("ʕ •ᴥ•ʔ")):
... print(i)
(1, 'a', 0.5, 'ʕ •ᴥ•ʔ')
(2, 'b', 0.7, 'ʕ •ᴥ•ʔ')
(1, 'c', 0.9, 'ʕ •ᴥ•ʔ')
(2, 'd', 0.5, 'ʕ •ᴥ•ʔ')
(1, 'e', 0.7, 'ʕ •ᴥ•ʔ')
## 想更加了解 itertools 的讀者,可以參考官方文件
## https://docs.python.org/zh-cn/3/library/itertools.html


結語

在這則筆記裡,我們學會了:

  • zip() 的基本用法
  • 矩陣轉置
  • dict 反轉
  • 還有長短不一的 list 同時迭代進階技巧

zip() 函式不只方便好用,筆者好豪也常在網友分享的 面試題 看到與它相關的問題出現,可以說是邁向進階 Python 必學的內建函式,相信讀者掌握這則筆記的技巧後,不論是刷演算法題或資料分析工作,都能多增加一項寫 Python 利器!

參考資料:


還想知道更多 Python 相關技巧嗎?推薦你閱讀好豪蒐集的《Python 神乎其技》免費教學文章,學會更多 Pythonic Code!

如果這篇文章有幫助到你,歡迎追蹤 好豪的粉絲專頁,我會持續分享 Python 技巧、資料科學等知識。

也歡迎點選下方按鈕將本文加入書籤隨時複習、或者分享給更多正在學 Python 的朋友。


from: https://haosquare.com/python-zip-function/

No comments:

Post a Comment