聯合迭代器與生成器,enumerate() 內建函式真香!

豌豆花下貓發表於2021-08-22

花下貓語:Python 中很多內建函式的作用都非常大,比如說 enumerate() 和 zip(),它們使得我們在作迭代操作時極為順手。這是一篇很多年前的 PEP,提議在 Python 2.3 版本中引入 enumerate(),該文件整合了其它幾篇 PEP 的想法(包括當時新引入的迭代器與生成器),提出了更好的實現方案以及函式名。經過這麼多年的發展,enumerate() 不可避免地有了一些變化,但不變的是,它跟 19 年前一樣,還是很有必要、很好用,用著真香!

PEP原文: https://www.python.org/dev/peps/pep-0279

PEP標題: The enumerate() built-in function

PEP作者: Raymond Hettinger

建立日期: 2002-06-30

合入版本: 2.3

譯者:豌豆花下貓@Python貓

PEP翻譯計劃https://github.com/chinesehuazhou/peps-cn

摘要

本 PEP 引進了一個新的內建函式 enumerate() 來簡化常用的迴圈寫法。它為所有的可迭代物件賦能,作用就像字典的 iteritems() 那樣——一種緊湊、可讀、可靠的索引表示法。

基本原理

Python 2.2 在 PEP 234[3] 中提出了可迭代物件介面的概念。iter() 工廠函式作為一種通用的呼叫約定而被提出,深入修改了迭代器的使用方式,作為整個 Python 的統一規範。這種統一的規範就是為對映型別、序列型別和檔案物件建立一個通用的可迭代物件介面。

PEP 255[1] 中提出的生成器是作為一種更容易建立迭代器的方法引入的,特別是具有複雜的內部執行過程或變數狀態的迭代器。有了生成器以後,PEP 212[2] 中關於迴圈的計數器的想法就有可能改進了。

那些想法是提供一種乾淨的迭代語法,帶有索引和值,但不適用於所有的可迭代物件。而且,那種方法沒有生成器提供的記憶體友好的優點(生成器不會一次性計算整個序列)。

(Python貓注:關於生成器的 PEP 也有翻譯,請檢視 PEP-255PEP-342PEP-380

新的提議是新增一個內建函式 enumerate(),在有了迭代器和生成器以後,它就可以實現。它為所有的可迭代物件賦能,作用就像字典的 iteritems() 那樣——一種緊湊、可讀、可靠的索引表示法。像 zip() 一樣,它有望成為一種常用的迴圈習語(idiom)。

(Python貓注:zip() 函式非常強,推薦閱讀《一篇文章掌握 Python 內建 zip() 的全部內容》)

這一提議的目的是利用現有的實現,再加一點點的努力來整合。它是向後相容的,不需要新的關鍵字。本提案將合入 Python 2.3,不需要從 __future__ 中匯入。

新內建函式的規範

def enumerate(collection):
   'Generates an indexed series:  (0,coll[0]), (1,coll[1]) ...'
   i = 0
   it = iter(collection)
   while 1:
      yield (i, it.next())
      i += 1

注A :PEP 212 迴圈計數器迭代[2]討論了幾個實現索引的提議。有些提議只適用於列表,不像上面的函式適用於任意生成器、xrange、序列或可迭代物件。

另外,那些提議是在 Python 2.2 之前提出並評估的,但是 Python 2.2 沒有包含生成器。因此,PEP 212 中的非生成器版本有一個缺點,即會用一個巨大的元組列表,導致消耗太多記憶體。

這裡提供的生成器版本快速且輕便,適用於所有可迭代物件,並允許使用者在不浪費計算量的情況下中途放棄。

還有一些涉及相關問題的 PEP:整型迭代器、整型 for 迴圈,以及一個修改 range 和 xrange 的引數的 PEP。enumerate() 提案並不排斥其它提案,即使那些提案被採納,它仍然滿足一個重要的需求——對任意可迭代物件中的元素進行計數的需求。

其它的提案給出了一種產生索引的方法,但沒有相應的值。如果給定的序列不支援隨機訪問,比如檔案物件、生成器或用__getitem__定義的序列,這就特別成問題。

注B :幾乎所有的 PEP 審閱人都歡迎這個函式,但對於“是否應該把它作為內建函式”

存在分歧。一方提議使用獨立的模組,主要理由是減緩語言膨脹的速度。

另一方提議使用內建函式,主要理由是該函式符合 Python 核心程式設計風格,適用於任何具有可迭代介面的物件。正如 zip() 解決了在多個序列上迴圈的問題,enumerate() 函式解決了迴圈計數器的問題。

如果只允許加一個內建函式,那麼 enumerate() 就是最重要的通用工具,可以解決最廣泛的問題,同時提高程式的簡潔性、清晰度和可靠性。

注C :討論了多種備選名稱:

函式名 分析
iterindexed() 五個音節太拗口了
index() 很好的動詞,但是可能會跟 .index () 方法混淆
indexed() 很受歡迎,但是應該避免形容詞
indexer() 在 for 迴圈中,名詞讀起來不太好
count() 直接而明確,但常用於其它語境
itercount() 直接、明確,但被不止一個人討厭
iteritems() 與字典的 key:value 概念衝突
itemize() 讓人困惑,因為 amap.items() != list(itemize(amap))
enum() 簡練;不及enumerate 清楚;與其它語言中的列舉太相似,但有著不同的含義

所有涉及“count”的名稱還有一個缺點,即隱含著計數是從 1 開始而不是從 0 開始的意思。

所有涉及“index”的名稱與資料庫語言的用法衝突,資料庫的索引表示一種排序操作,但不是線性排序。

注D: 在最初的提案中,這個函式帶有可選的 start 和 stop 引數。GvR 指出,函式enumerate(seqn,4,6) 還有一種看似合理的解釋,即返回序列的第 4 和第 5 個元素的切片。為了避免歧義,這兩個可選引數被摘掉了,儘管這意味著迴圈計數器失去了部分的靈活性。

在從 1 開始計數的常見用例中,這種可選引數的寫法很有用,比如:

for linenum, line in enumerate(source,1):
    print linenum, line

(Python貓注:這篇文件說 enumerate 沒有起止引數,然而,在後續版本中(例如我用的 3.9),它支援使用一個可選的 start 引數!我暫未查到這個變更是在何時加入的,如有知情者,煩請告知我,以便修正!)

GvR 評論道:

filter 和 map 應該 die,被納入列表推導式,不增加更多的變體。我寧可引進做迭代器運算的內建函式(例如 iterzip,我經常舉的例子)。
我認可用某種方法並行地遍歷序列及其索引的想法。把它作為一個內建函式,沒有問題。
我不喜歡“indexed”這個名字;形容詞不是好的函式名。可以用 iterindexed() ?

Ka-Ping Yee 評論道:

我對你的提議也很滿意……新增的內建函式(傾向於用“indexed”)是我期盼了很久的東西。

Neil Schemenauer 評論道:

新的內建函式聽起來不錯。Guido 可能會擔心增加太多內建物件。你最好把它們作為某個模組的一部分。如果你用模組的話,那麼你可以新增很多有用的函式(Haskell 有很多,我們可以去“偷”)。

Magnus Lie Hetland 評論道:

我認為 indexed 會是一個有用和自然的內建函式。我肯定會經常使用它。
我非常喜歡 indexed();+1。 很高興它淘汰了 PEP-281。為迭代器新增一個單獨的模組似乎是個好主意。

來自社群的反饋:

對於 enumerate() 提案,幾乎 100% 贊成。幾乎所有人都喜歡這個想法。

作者的註釋:

在這些評論之前,共有四種內建函式被提出來。經過評論之後,xmap、xfilter 和 xzip 被撤銷了。剩下的一個對 Python 來說是至關重要的。Indexed() 非常容易實現,並且立馬就可以寫進文件。更重要的是,它在日常程式設計中很有用,如果不用它,就需要顯式地使用生成器。
這個提案最初包含了另一個函式 iterzip()。但之後在 itertools 模組中實現成了一個 izip() 函式。

參考材料

1、PEP 255 Simple Generators http://www.python.org/dev/peps/pep-0255

2、(1, 2) PEP 212 Loop Counter Iteration http://www.python.org/dev/peps/pep-0212

3、PEP 234 Iterators http://www.python.org/dev/peps/pep-0234

版權

本文件已經進入公共領域。源文件:

https://github.com/python/peps/blob/master/pep-0279.txt

相關文章