Python enumerate():使用計數器簡化迴圈

華為雲開發者社群發表於2021-12-10

摘要:當您需要計數和迭代中的值時,Pythonenumerate()允許您編寫 Pythonicfor迴圈。最大的優點enumerate()是它返回一個帶有計數器和值的元組,因此您不必自己增加計數器。它還為您提供了更改計數器起始值的選項。

本文分享自華為雲社群《Python enumerate():使用計數器簡化迴圈》,作者:Yuchuan。

在 Python 中,for迴圈通常被寫成對可迭代物件的迴圈。這意味著您不需要計數變數來訪問迭代中的專案。但有時,您確實希望有一個在每次迴圈迭代中都會發生變化的變數。您可以使用 Python enumerate()來同時從可迭代物件中獲取計數器和值,而不是自己建立和增加變數!

在本教程中,您將看到如何:

  • 用於enumerate()在迴圈中獲取計數器
  • 適用enumerate()於顯示專案計數
  • enumerate()與條件語句一起使用
  • 實現自己的同等功能,以enumerate()
  • 解包返回的值enumerate()

讓我們開始吧!

for在 Python 中使用迴圈進行迭代

forPython 中的迴圈使用基於集合的迭代。這意味著 Python 在每次迭代時將迭代中的下一項分配給迴圈變數,如下例所示:

>>>
>>> values = ["a", "b", "c"]

>>> for value in values:
...     print(value)
...
a
b
c

在這個例子中,values是一個名單有三個字串,"a","b",和"c"。在 Python 中,列表是一種可迭代物件。在for迴圈中,迴圈變數是value。在迴圈的每次迭代中,value設定為 的下一項values。

接下來,您列印 value到螢幕上。基於集合的迭代的優勢在於它有助於避免其他程式語言中常見的逐一錯誤。

現在想象一下,除了值本身之外,您還想在每次迭代時將列表中專案的索引列印到螢幕上。處理此任務的一種方法是建立一個變數來儲存索引並在每次迭代時更新它:

>>>
>>> index = 0

>>> for value in values:
...     print(index, value)
...     index += 1
...
0 a
1 b
2 c

在本例中,index是一個整數,用於跟蹤您在列表中的距離。在迴圈的每次迭代中,您列印index以及value. 迴圈的最後一步是將儲存的數字更新index一。當您忘記index在每次迭代時更新時,會出現一個常見錯誤:

>>>
>>> index = 0

>>> for value in values:
...     print(index, value)
...
0 a
0 b
0 c

在這個例子中,index在0每次迭代時都保持 at ,因為沒有程式碼在迴圈結束時更新它的值。特別是對於長或複雜的迴圈,這種錯誤是出了名的難以追蹤。

解決此問題的另一種常見方法是使用range()結合len()自動建立索引。這樣,您就不需要記住更新索引:

>>>
>>> for index in range(len(values)):
...     value = values[index]
...     print(index, value)
...
0 a
1 b
2 c

在本例中,len(values)返回 的長度values,即3。然後range()建立一個迭代器,從預設的起始值開始執行,0直到它達到len(values)負一。在這種情況下,index成為您的迴圈變數。在迴圈中,您將當前值設定為value等於 中的專案。最後,您列印和。valuesindexindexvalue

在此示例中,可能發生的一個常見錯誤是您value在每次迭代開始時忘記更新。這類似於之前忘記更新索引的錯誤。這是該迴圈不被視為Pythonic 的原因之一。

這個例子也有一些限制,因為values必須允許使用整數索引訪問它的專案。允許這種訪問的可迭代物件在 Python中稱為序列

技術細節:根據Python 文件,可迭代物件是可以一次返回一個成員的任何物件。根據定義,可迭代物件支援迭代器協議,該協議指定在迭代器中使用物件時如何返回物件成員。Python 有兩種常用的可迭代型別:

  1. 序列
  2. 發電機

任何可迭代物件都可以在for迴圈中使用,但只能通過整數索引訪問序列。嘗試通過生成器或迭代器的索引訪問專案將引發TypeError:

>>>
>>> enum = enumerate(values)
>>> enum[0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'enumerate' object is not subscriptable

在此示例中,您將返回值分配enumerate()給enum。enumerate()是一個迭代器,因此嘗試通過索引訪問其值會引發TypeError.

幸運的是,Pythonenumerate()可以讓您避免所有這些問題。它是一個內建函式,這意味著自從2003 年在 Python 2.3中新增它以來,它在每個版本的 Python 中都可用。

使用 Python 的 enumerate()

您可以enumerate()以與使用原始可迭代物件幾乎相同的方式在迴圈中使用。不是將可迭代物件直接in放在for迴圈之後,而是將它放在enumerate(). 您還必須稍微更改迴圈變數,如下例所示:

>>>
>>> for count, value in enumerate(values):
...     print(count, value)
...
0 a
1 b
2 c

當您使用 時enumerate(),該函式會返回兩個迴圈變數:

  1. 該計數當前迭代的
  2. 當前迭代中專案的值

就像普通for迴圈一樣,迴圈變數可以任意命名。您在本例中使用count和value,但它們可以命名為i和/v或任何其他有效的 Python 名稱。

使用enumerate(),您不需要記住從可迭代物件訪問該專案,並且您不需要記住在迴圈結束時推進索引。一切都由 Python 的魔力自動為您處理!

技術細節:使用兩個迴圈變數count和value,用逗號分隔是引數解包的一個例子。本文稍後將進一步討論這個強大的 Python 特性。

Pythonenumerate()有一個額外的引數,您可以使用它來控制計數的起始值。預設情況下,起始值是0因為 Python 序列型別從零開始索引。換句話說,當您想要檢索列表的第一個元素時,您可以使用 index 0:

>>>
>>> print(values[0])
a

您可以在此示例中看到,使用values索引訪問0會給出第一個元素a。但是,很多時候您可能不希望從enumerate()開始計數0。例如,您可能希望列印一個自然計數數作為使用者的輸出。在這種情況下,您可以使用start引數 forenumerate()來更改起始計數:

>>>
>>> for count, value in enumerate(values, start=1):
...     print(count, value)
...
1 a
2 b
3 c

在本例中,您傳遞start=1,它從第一次迴圈迭代count的值開始1。將此與前面的示例進行比較,其中start的預設值為0,看看您是否能發現差異。

用 Python 練習 enumerate()

您應該enumerate()在需要在迴圈中使用計數和專案的任何時候使用。請記住,enumerate()每次迭代都會將計數加一。但是,這只是略微限制了您的靈活性。由於計數是標準的 Python 整數,因此您可以通過多種方式使用它。在接下來的幾節中,您將看到enumerate().

可迭代項的自然計數

在上一節中,您看到了如何使用enumerate()withstart建立一個自然計數數字來為使用者列印。enumerate()在 Python 程式碼庫中也像這樣使用。您可以在指令碼中看到一個示例,它讀取 reST 檔案並在出現格式問題時告訴使用者。

注意: reST,也稱為reStructured Text,是 Python 用於文件的文字檔案的標準格式。您經常會看到在 Python 類和函式中包含作為文件字串的 reST 格式的字串。讀取原始碼檔案並告訴使用者格式問題的指令碼稱為linter,因為它們在程式碼中尋找隱喻的lint。

這個例子是從rstlint. 不要太擔心這個函式如何檢查問題。關鍵是要展示在現實世界中的使用enumerate():

 1def check_whitespace(lines):
 2    """Check for whitespace and line length issues."""
 3    for lno, line in enumerate(lines):
 4        if "\r" in line:
 5            yield lno+1, "\\r in line"
 6        if "\t" in line:
 7            yield lno+1, "OMG TABS!!!1"
 8        if line[:-1].rstrip(" \t") != line[:-1]:
 9            yield lno+1, "trailing whitespace"

check_whitespace()接受一個引數,lines,它是應該評估的檔案行。在 的第三行check_whitespace(),enumerate()用於迴圈 over lines。這將返回行號,縮寫為lno和line。由於start未使用,因此lno是檔案中行的從零開始的計數器。check_whitespace()然後對不合適的字元進行多次檢查:

  1. 回車 ( \r)
  2. 製表符 ( \t)
  3. 行尾的任何空格或製表符

當這些專案之一存在時,為使用者check_whitespace() 產生當前行號和有用的訊息。計數變數lno已1新增到其中,以便它返回計數行號而不是從零開始的索引。當 的使用者rstlint.py閱讀訊息時,他們將知道要轉到哪一行以及要修復的內容。

跳過專案的條件語句

使用條件語句來處理專案是一種非常強大的技術。有時您可能只需要在迴圈的第一次迭代上執行操作,如下例所示:

>>>
>>> users = ["Test User", "Real User 1", "Real User 2"]
>>> for index, user in enumerate(users):
...     if index == 0:
...         print("Extra verbose output for:", user)
...     print(user)
...
Extra verbose output for: Test User
Real User 1
Real User 2

在此示例中,您將列表用作使用者的模擬資料庫。第一個使用者是您的測試使用者,因此您希望列印有關該使用者的額外診斷資訊。由於您已將系統設定為首先測試使用者,因此您可以使用迴圈的第一個索引值來列印額外的詳細輸出。

您還可以將數學運算與計數或索引的條件結合起來。例如,您可能需要從可迭代物件中返回專案,但前提是它們具有偶數索引。您可以使用enumerate()以下方法執行此操作:

>>>
>>> def even_items(iterable):
...     """Return items from ``iterable`` when their index is even."""
...     values = []
...     for index, value in enumerate(iterable, start=1):
...         if not index % 2:
...             values.append(value)
...     return values
...

even_items()接受一個名為 的引數,iterable它應該是 Python 可以迴圈遍歷的某種型別的物件。首先,values被初始化為一個空列表。然後你用和 set建立一個for迴圈。iterableenumerate()start=1

內for迴圈,你檢查除以餘下是否index通過2為零。如果是,則將該專案附加到values. 最後,您返回 values。

您可以使用列表推導式在一行中執行相同的操作,而無需初始化空列表,從而使程式碼更加Pythonic:

>>>
>>> def even_items(iterable):
...     return [v for i, v in enumerate(iterable, start=1) if not i % 2]
...

在此示例程式碼中,even_items()使用列表推導式而不是for迴圈從列表中提取索引為偶數的每個專案。

您可以even_items()通過從1到的整數範圍中獲取偶數索引項來驗證它是否按預期工作10。結果將是[2, 4, 6, 8, 10]:

>>>
>>> seq = list(range(1, 11))

>>> print(seq)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

>>> even_items(seq)
[2, 4, 6, 8, 10]

正如預期的那樣,從even_items()返回偶數索引項seq。當您使用整數時,這不是獲得偶數的最有效方法。但是,現在您已經驗證它even_items()可以正常工作,您可以獲得 ASCII 字母表的偶數索引字母:

>>>
>>> alphabet = "abcdefghijklmnopqrstuvwxyz"

>>> even_items(alphabet)
['b', 'd', 'f', 'h', 'j', 'l', 'n', 'p', 'r', 't', 'v', 'x', 'z']

alphabet是一個字串,它包含 ASCII 字母表的所有 26 個小寫字母。呼叫even_items()和傳遞alphabet返回字母表中交替字母的列表。

Python 字串是序列,可用於迴圈以及整數索引和切片。因此,對於字串,您可以使用方括號even_items()更有效地實現相同的功能:

>>>
>>> list(alphabet[1::2])
['b', 'd', 'f', 'h', 'j', 'l', 'n', 'p', 'r', 't', 'v', 'x', 'z']

在這裡使用字串切片,你給出起始索引1,它對應於第二個元素。第一個冒號之後沒有結束索引,因此 Python 會轉到字串的末尾。然後新增第二個冒號,後跟 a,2以便 Python 將採用所有其他元素。

但是,正如您之前看到的,生成器和迭代器不能被索引或切片,因此您仍然會發現它們enumerate()很有用。要繼續上一個示例,您可以建立一個生成器函式,根據需要生成字母表中的字母:

>>>
>>> def alphabet():
...     alpha = "abcdefghijklmnopqrstuvwxyz"
...     for a in alpha:
...         yield a

>>> alphabet[1::2]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'function' object is not subscriptable

>>> even_items(alphabet())
['b', 'd', 'f', 'h', 'j', 'l', 'n', 'p', 'r', 't', 'v', 'x', 'z']

在此示例中,您定義alphabet()了一個生成器函式,當該函式在迴圈中使用時,它會一個一個地生成字母表中的字母。Python 函式,無論是生成器還是常規函式,都無法通過方括號索引訪問。你在第二行試試這個,它會引發一個TypeError.

不過,您可以在迴圈中使用生成器函式,並且您可以在最後一行傳遞alphabet()給even_items(). 可以看到結果和前面兩個例子是一樣的。

理解 Python enumerate()

在最後幾節中,您看到了何時以及如何enumerate()發揮優勢的示例。現在您已經掌握了 的實際方面enumerate(),您可以瞭解更多有關該函式如何在內部工作的資訊。

為了更好地瞭解enumerate()工作原理,您可以使用 Python 實現您自己的版本。您的版本enumerate()有兩個要求。這應該:

  1. 接受一個可迭代和一個起始計數值作為引數
  2. 發回一個包含當前計數值和可迭代物件相關項的元組

Python 文件中給出了一種編寫滿足這些規範的函式的方法:

>>>
>>> def my_enumerate(sequence, start=0):
...     n = start
...     for elem in sequence:
...         yield n, elem
...         n += 1
...

my_enumerate()接受兩個引數,sequence和start。預設值start是0。在函式定義中,您初始化n為 的值start並for在sequence.

對於每一個elem在sequence你yield控制返回給呼叫位置和傳送回的當前值n和elem。最後,您遞增n以準備下一次迭代。您可以my_enumerate()在此處檢視實際操作:

>>>
>>> seasons = ["Spring", "Summer", "Fall", "Winter"]

>>> my_enumerate(seasons)
<generator object my_enumerate at 0x7f48d7a9ca50>

>>> list(my_enumerate(seasons))
[(0, 'Spring'), (1, 'Summer'), (2, 'Fall'), (3, 'Winter')]

>>> list(my_enumerate(seasons, start=1))
[(1, 'Spring'), (2, 'Summer'), (3, 'Fall'), (4, 'Winter')]

首先,您建立要使用的四個季節的列表。接下來,您將展示呼叫my_enumerate()with seasonsassequence建立一個生成器物件。這是因為您使用yield關鍵字將值傳送回撥用者。

最後,建立兩個列表my_enumerate(),在其中起始值被保留為預設,0在其中,一個start改變為1。在這兩種情況下,您最終都會得到一個元組列表,其中每個元組的第一個元素是計數,第二個元素是來自 的值seasons。

儘管您enumerate()只需幾行 Python 程式碼即可實現等效的函式,但實際的程式碼enumerate() 是用 C 編寫的。這意味著它超級快速和高效。

解包引數 enumerate()

當您enumerate()在for迴圈中使用時,您告訴 Python 使用兩個變數,一個用於計數,另一個用於值本身。您可以通過使用稱為引數解包的 Python 概念來做到這一點。

引數解包的思想是,一個元組可以根據序列的長度分成幾個變數。例如,您可以將包含兩個元素的元組解包為兩個變數:

>>>
>>> tuple_2 = (10, "a")
>>> first_elem, second_elem = tuple_2
>>> first_elem
10
>>> second_elem
'a'

首先,您建立一個包含兩個元素的元組,10和"a"。然後將該元組解包到first_elemand 中second_elem,每個都從元組中分配一個值。

當您呼叫enumerate()並傳遞一系列值時,Python 會返回一個迭代器。當您向迭代器詢問其下一個值時,它會生成一個包含兩個元素的元組。元組的第一個元素是計數,第二個元素是您傳遞的序列中的值:

>>>
>>> values = ["a", "b"]
>>> enum_instance = enumerate(values)
>>> enum_instance
<enumerate at 0x7fe75d728180>
>>> next(enum_instance)
(0, 'a')
>>> next(enum_instance)
(1, 'b')
>>> next(enum_instance)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

在此示例中,您建立了一個列表,該列表values包含兩個元素"a"和"b"。然後傳遞values給enumerate()並將返回值分配給enum_instance. 當您列印時enum_instance,您可以看到它是一個enumerate()具有特定記憶體地址的例項。

然後使用 Python 的內建next()函式從enum_instance. enum_instance返回的第一個值是一個元組,其中包含計數0和來自 的第一個元素values,即"a"。

next()再次呼叫on 會enum_instance產生另一個元組,這次是計數1和來自values,的第二個元素"b"。最後,由於沒有更多的值要從 返回,所以再呼叫next()一次會增加。StopIterationenum_instance

在for迴圈中使用可迭代物件時,Python 會next()在每次迭代開始時自動呼叫,直到StopIteration引發。Python 將從可迭代物件中檢索到的值分配給迴圈變數。

如果可迭代物件返回一個元組,則可以使用引數解包將元組的元素分配給多個變數。這是您在本教程前面通過使用兩個迴圈變數所做的。

另一次您可能已經看到使用for迴圈解包引數是使用內建的zip(),它允許您同時迭代兩個或多個序列。在每次迭代中,zip()返回一個元組,該元組從所有傳遞的序列中收集元素:

>>>
>>> first = ["a", "b", "c"]
>>> second = ["d", "e", "f"]
>>> third = ["g", "h", "i"]
>>> for one, two, three in zip(first, second, third):
...     print(one, two, three)
...
a d g
b e h
c f i

通過使用zip(),可以遍歷first,second以及third在同一時間。在for迴圈中,您分配元素 from firstto one、 from secondtotwo和 from thirdto three。然後列印三個值。

您可以組合zip()和enumerate()使用巢狀引數解包:

>>>
>>> for count, (one, two, three) in enumerate(zip(first, second, third)):
...     print(count, one, two, three)
...
0 a d g
1 b e h
2 c f i

在for此示例的迴圈中,您巢狀zip()在enumerate(). 這意味著每次for迴圈迭代時,都會enumerate()產生一個元組,其中第一個值作為計數,第二個值作為另一個元組,其中包含從引數到 的元素zip()。要解壓巢狀結構,您需要新增括號以從zip().

還有其他方法可以模擬enumerate()與zip(). 一種方法使用itertools.count(),它預設返回從零開始的連續整數。您可以將前面的示例更改為使用itertools.count():

>>>
>>> import itertools
>>> for count, one, two, three in zip(itertools.count(), first, second, third):
...     print(count, one, two, three)
...
0 a d g
1 b e h
2 c f i

用itertools.count()在這個例子中,您可以使用一個單一的zip()呼叫產生計數以及沒有巢狀引數拆包的迴圈變數。

結論

當您需要計數和迭代中的值時,Pythonenumerate()允許您編寫 Pythonicfor迴圈。最大的優點enumerate()是它返回一個帶有計數器和值的元組,因此您不必自己增加計數器。它還為您提供了更改計數器起始值的選項。

在本教程中,您學習瞭如何:

  • enumerate()在for迴圈中使用 Python
  • 應用enumerate()在幾個現實世界的例子中
  • enumerate()使用引數解包獲取值
  • 實現自己的同等功能,以enumerate()

您還看到enumerate()在一些實際程式碼中使用,包括在CPython程式碼儲存庫中。您現在擁有簡化迴圈並使 Python 程式碼時尚的超能力!

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章