Python -  看到一個 grouper 的寫法

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

import itertools
def grouper(iterable, n):
    return itertools.zip_longest(*[iter(iterable)] * n)
簡單範例
grouper([0,1,2,3,4,5,6,7,8], 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"))

iter 是幹麻的?

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

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

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

  • 對容器呼叫 __iter__() 取得 iterator。 (如果有支援的話)
  • 使用 __next__() 做取值與尋訪。
  • 處理 StopIteration exception 的終止訊號
bs = [0,1,2,3,4]
for loop expression
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

準備一個 list 與接收三個參數的 function
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
1
zip_longest(* [iter([0,1,2,3,4,5,6,7,8,9])] * 3)
2
iter1 = iter([0,1,2,3,4,5,6,7,8,9])
zip_longest(* [iter1] * 3)
3
zip_longest(* [iter1, iter1, iter1])
4
zip_longest(iter1, iter1, iter1)

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

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

第二個 3-tuple

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

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

final
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

comments powered by Disqus