Python — 看到一個 grouper 的寫法

OT Chen
6 min readFeb 5, 2017

--

在 Stack Overflow 看到 What is the most “pythonic” way to iterate over a list in chunks? 看到這段用法,實在驚為天人,覺得真是優雅(又看不太懂XD)。知道他是融合了 zip, iterator, list repeat 跟 parameter unpack 這四種用法,而且也是 lazily 取值的方式在效能上也不浪費。

import itertoolsdef grouper(iterable, n):
return itertools.zip_longest(*[iter(iterable)] * n)

簡單範例

grouper([1,2,3,4,5,6,7,8,9], 3) 
->
[[0,1,2],[3,4,5],[6,7,8]]

以下就簡單記錄一下,為了弄懂這段 Code ,回去復習一下 zip, iterator 的筆記。

zip_longest 是幹麻的

def zip_longest(*iterable, fillvalue=None)

將多個陣列像拉鍊一樣,平行地黏上,使得各陣列同位置的元素被包在一個個小小的 tuple裡。

  • 長度是當中陣列最長的長度,內部 tuple 大小則是看有傳入陣列有幾個。
  • 由於是取最長的陣列長度當輸出長度,所以可能會有空隙,這時候可傳入自定義的 fillvalue 來填滿剩下的空間。
  • 參數是 *iterable,代表則是你可以傳任意數量的陣列進去。
  • 回傳一個 iterator 所以可以 lazily 地取值。
zip_longest([1,2,3,4,5], [155,166,177,188],[2016,2017,2018])
->
(
(1,155,2016),
(2,166,2017),
(3,177,2018),
(4,188,None),
(5,None,None)
)
zip_longest(“abcde”, “ABC”, fillevalue=”X”)
->
(
("a","A"),
("b","B"),
("c”,"C"),
("d”,"X"),
("e","X")
)

zip_longest 與內建 zip 的不同

zip 只取最短長度

若是其中一個陣列取完了,整個過程也就結束,當然也就沒有 padding 的需求

zip([1,2,3,4,5], [155,166,177,188],[2016,2017,2018])
->
((1,155,2016), (2,166,2017), (3,177,2018))
zip(“abcde”, “ABC”)
->
((“a”,”A”),(“b”,”B”),(“c”,”C”))

zip()

itertools.zip_longest()

iter 是幹麻的?

對陣列套用 iter() ,會得到一個針對此陣列,記錄初始位置的 iterator。

隨著你對它做 next(),會得到它的當前值,並且此 iterator 會改變它狀態,將位置移置下一個。直到 iterator 取值到終點時,它會拋出 StopIteration作為結束的訊號。

一般來說 iterator 的操作比較少直接用到,通常會被包裝在 for loop 裡面,由 compiler 來幫你包裝好整個容器的尋訪。

  • 對容器呼叫 __iter__() 取後 iterator。 (如果有支援的話)
  • 使用 __next__() 做取值與尋訪。
  • 處理 StopIteration exception 的終止訊號
bs = [0,1,2,3,4]for x in bs:
print(x)

等價於

iter1 = bs.__iter__()
try:
while True:
x = next(iter1)
print(x)
except StopIteration:
pass

Using-an-iterator-to-print-integers

  • 同一個 iterator 在不同地方操作是會彼此影響的。
  • 不同的 iterator,彼此位置的資訊是獨立的。

Iterator

前面的星號 —Argument unpacking

用來將陣列解開,並把所有元素當參數傳入 function。

Argument unpacking

bs = [1, 2, 3]def add3(a, b, c)
return a + b + c

hello(bs[0], bs[1], bs[2])

等價於

hello(*bs)

後面的星號 — list element repeat creation

[x] * n

會重複 [] 裡的元素 x 一共 n 次,再放在 [] 裡。

等價於 [x, x, x, …, (第n個)x]

複製是以 reference 的方式,所以每個 x 都會指向同一個物件

[iter(iterable)] * n

等價於

iter1 = iter(iterable])
[iter1, iter1, iter1, …, (第n個) iter1]

Create List of Single Item Repeated n Times in Python

所以是怎麼組合的?

zip_longest(*[iter(iterable)] * n)

以下面兩個參數為例子

  • iterable = [0,1,2,3,4,5,6,7,8,9]
  • n = 3
zip_longest(* [iter([0,1,2,3,4,5,6,7,8,9])] * 3)
iter1 = iter([0,1,2,3,4,5,6,7,8,9])
zip_longest(* [iter1] * 3)
zip_longest(* [iter1, iter1, iter1])
zip_longest(iter1, iter1, iter1)

接著 zip_longest 依序會從左到右拿起同一個 iter1 ,填好第一個 3-tuple 。

zip_longest(iter1, iter1, iter1) 
->
(
(0, 1, 2),
...

第二個 3-tuple

zip_longest(iter1, iter1, iter1) 
->
(
(0, 1, 2),
(3, 4, 5),
...
zip_longest(iter1, iter1, iter1) 
->
(
(0, 1, 2),
(3, 4, 5),
(6, 7, 8),
...

一直到 iter1 填到最後一個 9 結束,產生最後一個 3-tuple,剩下兩個欄位就填 fillevalue 的預設值: None 。

zip_longest(iter1, iter1, iter1) 
->
(
(0, 1, 2),
(3, 4, 5),
(6, 7, 8),
(9, None, None)
)

Tada!

一個利用 iterator 做 round-robin 的方式,依照你給定的 n 值,將每 n 個元素包裝成一個個的 n-tuple ,進而達到 group 的目的,就完成啦。

這段 code 似乎也被收錄在官方文件的 itertools 各種 receipt ,當作是延伸用方法。也可以點下面連結去看一下其他的神應用。

Itertools Recipes

--

--