geekdoc-python-zh/docs/realpython/python-enumerate.md

472 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Python enumerate():用计数器简化循环
> 原文:<https://realpython.com/python-enumerate/>
*立即观看**本教程有真实 Python 团队创建的相关视频课程。配合文字教程一起看,加深理解: [**用 Python enumerate()循环**](/courses/looping-with-python-enumerate/)
在 Python 中, [`for`循环](https://realpython.com/python-for-loop/)通常被写成对可迭代对象的循环。这意味着您不需要计数变量来访问 iterable 中的项。但是,有时您确实希望有一个在每次循环迭代中都发生变化的变量。您可以使用 Python 的 **`enumerate()`** 同时从 iterable 中获取计数器和值,而不是自己创建和递增变量!
**在本教程中,您将了解如何:**
* 使用 **`enumerate()`** 在循环中获得一个计数器
* 将`enumerate()`应用到**显示项目计数**
* 将`enumerate()`与**条件语句**一起使用
* 实现自己的**等价函数**到`enumerate()`
* **解包`enumerate()`返回的值**
我们开始吧!
**免费下载:** [从 CPython Internals:您的 Python 3 解释器指南](https://realpython.com/bonus/cpython-internals-sample/)获得一个示例章节,向您展示如何解锁 Python 语言的内部工作机制,从源代码编译 Python 解释器,并参与 CPython 的开发。
## 用 Python 中的`for`循环迭代
Python 中的`for`循环使用了**基于集合的迭代**。这意味着 Python 会在每次迭代时将 iterable 中的下一项分配给循环变量,如下例所示:
>>>
```py
>>> values = ["a", "b", "c"]
>>> for value in values:
... print(value)
...
a
b
c
```
在这个例子中,`values`是一个带有三个[字符串](https://realpython.com/python-strings/)、`"a"`、`"b"`和`"c"`的[列表](https://realpython.com/python-lists-tuples/)。在 Python 中,列表是一种可迭代对象。在`for`循环中,循环变量是`value`。在循环的每次迭代中,`value`被设置为从`values`开始的下一项。
接下来,你[将](https://realpython.com/python-print/)打印到屏幕上。基于集合的迭代的优点是它有助于避免其他编程语言中常见的[逐个错误](https://en.wikipedia.org/wiki/Off-by-one_error)。
现在想象一下,除了值本身之外,您还想在每次迭代时将列表中项的索引打印到屏幕上。完成这项任务的一种方法是创建一个变量来存储索引,并在每次迭代中更新它:
>>>
```py
>>> index = 0
>>> for value in values:
... print(index, value)
... index += 1
...
0 a
1 b
2 c
```
在这个例子中,`index`是一个整数,记录你在列表中的位置。在循环的每次迭代中,你打印出`index`和`value`。循环的最后一步是将存储在`index`中的数字更新 1。当你忘记在每次迭代中更新`index`时,会出现一个常见的错误:
>>>
```py
>>> index = 0
>>> for value in values:
... print(index, value)
...
0 a
0 b
0 c
```
在这个例子中,`index`在每次迭代中都停留在`0`上,因为没有代码在循环结束时更新它的值。特别是对于长的或复杂的循环,这种错误是出了名的难以追踪。
解决这个问题的另一种常见方法是使用 [`range()`](https://docs.python.org/3/library/stdtypes.html#range) 结合 [`len()`](https://realpython.com/len-python-function/) 来自动创建索引。这样,您不需要记住更新索引:
>>>
```py
>>> for index in range(len(values)):
... value = values[index]
... print(index, value)
...
0 a
1 b
2 c
```
本例中,`len(values)`返回`values`的长度,也就是`3`。然后 [`range()`](https://realpython.com/python-range/) 创建一个迭代器,从默认的起始值`0`开始运行,直到到达`len(values)`减 1。在这种情况下`index`成为你的循环变量。在循环中,您将`value`设置为等于当前值`index`的`values`中的项目。最后,你打印出`index`和`value`。
在这个例子中,一个可能发生的常见错误是在每次迭代开始时忘记更新`value`。这类似于之前忘记更新索引的 bug。这是这个循环不被认为是[python 式](https://realpython.com/courses/how-to-write-pythonic-loops/)的一个原因。
这个例子也有一些限制,因为`values`必须允许使用整数索引来访问它的项目。允许这种访问的可重复项在 Python 中被称为**序列**。
**技术细节:**根据 [Python 文档](https://docs.python.org/3/glossary.html#term-iterable),一个 **iterable** 是任何可以一次返回一个成员的对象。根据定义iterables 支持[迭代器协议](https://docs.python.org/3/library/stdtypes.html#typeiter),该协议指定了在[迭代器](https://docs.python.org/3/glossary.html#term-iterator)中使用对象时如何返回对象成员。Python 有两种常用的可迭代类型:
1. [序列](https://docs.python.org/3/glossary.html#term-sequence)
2. [发电机](https://docs.python.org/3/glossary.html#term-generator)
任何 iterable 都可以在`for`循环中使用,但是只有序列可以被[整数](https://realpython.com/python-numbers/#integers)索引访问。试图通过索引从[生成器](http://www.dabeaz.com/generators/Generators.pdf)或[迭代器](https://dbader.org/blog/python-iterators)中访问项目将引发`TypeError`:
>>>
```py
>>> enum = enumerate(values)
>>> enum[0]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'enumerate' object is not subscriptable
```
在这个例子中,你把`enumerate()`的返回值赋给 [`enum`](https://realpython.com/python-enum/) 。`enumerate()`是一个迭代器,所以试图通过索引访问它的值会引发一个`TypeError`。
幸运的是Python 的 [`enumerate()`](https://docs.python.org/3/library/functions.html#enumerate) 让你避免了所有这些问题。这是一个**内置的**函数,这意味着自从 2003 年在 Python 2.3 中添加了[以来,它在 Python 的每个版本中都可用。](https://www.python.org/dev/peps/pep-0279/)
[*Remove ads*](/account/join/)
## 使用 Python 的`enumerate()`
你可以在一个循环中使用 [`enumerate()`](https://realpython.com/courses/how-to-write-pythonic-loops/) ,就像你使用原始的 iterable 对象一样。不是将 iterable 直接放在`for`循环中的`in`之后,而是放在`enumerate()`的括号内。您还必须稍微更改循环变量,如下例所示:
>>>
```py
>>> 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()`,您不需要记得从 iterable 中访问项也不需要记得在循环结束时推进索引。Python 的魔力会自动为您处理所有事情!
**技术细节:**使用逗号分隔的两个循环变量`count`和`value`是[参数解包](https://realpython.com/defining-your-own-python-function/#argument-tuple-unpacking)的一个例子。本文稍后将进一步讨论这个强大的 Python 特性。
Python 的`enumerate()`有一个额外的参数,可以用来控制计数的起始值。默认情况下,起始值是`0`,因为 Python 序列类型的索引是从零开始的。换句话说,当你想检索一个列表的第一个元素时,你可以使用 index `0`:
>>>
```py
>>> print(values[0])
a
```
在这个例子中可以看到,用索引`0`访问`values`给出了第一个元素`a`。然而,很多时候您可能不希望从`enumerate()`开始计数,而是从`0`开始计数。例如,您可能希望为用户输出一个自然计数。在这种情况下,您可以使用`enumerate()`的`start`参数来更改起始计数:
>>>
```py
>>> for count, value in enumerate(values, start=1):
... print(count, value)
...
1 a
2 b
3 c
```
在这个例子中,您传递了`start=1`,它在第一次循环迭代中以值`1`开始`count`。将它与前面的例子进行比较,在前面的例子中,`start`的默认值是`0`,看看您是否能发现不同之处。
## 用 Python `enumerate()`练习
每当你需要在循环中使用计数和一个项目时,你都应该使用`enumerate()`。请记住,`enumerate()`会在每次迭代时将计数递增 1。然而这只是稍微限制了您的灵活性。因为 count 是一个标准的 Python 整数,所以可以以多种方式使用它。在接下来的几节中,您将看到`enumerate()`的一些用法。
### 可迭代项目的自然计数
在上一节中,您看到了如何使用`enumerate()`和`start`来为用户创建一个自然计数。`enumerate()`在 Python 代码库中也是这样使用的。您可以在一个脚本中看到一个例子,它读取 reST 文件并在出现格式问题时告诉用户。
**注意:** reST也称为[重构文本](https://en.wikipedia.org/wiki/ReStructuredText),是 Python 用于文档的文本文件的标准格式。在 Python 类和函数中,您会经常看到 reST 格式的字符串作为[文档字符串](https://realpython.com/documenting-python-code/)出现。读取源代码文件并告诉用户格式问题的脚本被称为**linter**,因为它们在代码中寻找隐喻的 [lint](https://en.wikipedia.org/wiki/Lint_(software)) 。
这个例子是由 [`rstlint.py`](https://github.com/python/cpython/blob/2d6097d027e0dd3debbabc702aa9c98d94ba32a3/Doc/tools/rstlint.py#L96-L104) 略加修改而来。不要太担心这个函数如何检查问题。重点是展示`enumerate()`的真实使用情况:
```py
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()`在`lines`上方循环使用。这将返回行号,缩写为`lno`和`line`。因为没有使用`start`,所以`lno`是文件中行的从零开始的计数器。`check_whitespace()`然后对错位字符进行多次检查:
1. 回车(`\r`)
2. 制表符(`\t`)
3. 行尾有空格或制表符吗
当这些项目之一出现时,`check_whitespace()` [产生](https://realpython.com/introduction-to-python-generators/#understanding-the-python-yield-statement)当前行号和对用户有用的消息。计数变量`lno`中添加了`1`,因此它返回计数行号,而不是从零开始的索引。当`rstlint.py`的用户阅读消息时,他们会知道去哪一行和修复什么。
[*Remove ads*](/account/join/)
### 跳过项目的条件语句
使用条件语句处理项目可能是一种非常强大的技术。有时,您可能只需要在循环的第一次迭代中执行操作,如下例所示:
>>>
```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
```
在这个例子中,您使用一个列表作为用户的模拟数据库。第一个用户是您的测试用户,因此您想要打印关于该用户的额外诊断信息。因为您已经设置了系统,测试用户是第一个,所以您可以使用循环的第一个索引值来打印额外的详细输出。
您还可以将数学运算与计数或索引的条件结合起来。例如,您可能需要从 iterable 中返回项,但前提是它们有偶数索引。您可以通过使用`enumerate()`来完成此操作:
>>>
```py
>>> 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`被初始化为一个空列表。然后用`enumerate()`在`iterable`上创建一个`for`循环,并设置`start=1`。
在`for`循环中,检查`index`除以`2`的余数是否为零。如果是,那么您[将](https://realpython.com/python-append/)项添加到`values`中。最后,你[返回](https://realpython.com/python-return-statement/) `values`
你可以通过使用一个[列表理解](https://realpython.com/list-comprehension-python/)在一行中做同样的事情,而不用初始化空列表,从而使代码更加[python 化](https://realpython.com/python-pep8/):
>>>
```py
>>> def even_items(iterable):
... return [v for i, v in enumerate(iterable, start=1) if not i % 2]
...
```
在这个示例代码中,`even_items()`使用一个列表理解而不是一个`for`循环来从列表中提取索引为偶数的每个项目。
您可以通过从从`1`到`10`的整数范围中获取偶数索引项来验证`even_items()`是否按预期工作。结果将是`[2, 4, 6, 8, 10]`:
>>>
```py
>>> 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 字母表的偶数索引字母:
>>>
```py
>>> 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()`相同的功能:
>>>
```py
>>> list(alphabet[1::2])
['b', 'd', 'f', 'h', 'j', 'l', 'n', 'p', 'r', 't', 'v', 'x', 'z']
```
在这里使用[字符串切片](https://realpython.com/courses/python-strings/),给出起始索引`1`,它对应于第二个元素。第一个冒号后没有结束索引,所以 Python 转到了字符串的末尾。然后添加第二个冒号,后跟一个`2`,这样 Python 将接受所有其他元素。
然而,正如您之前看到的,生成器和迭代器不能被索引或切片,所以您仍然会发现`enumerate()`很有用。继续前面的例子,您可以创建一个[生成器函数](https://realpython.com/introduction-to-python-generators/),它按需生成字母表中的字母:
>>>
```py
>>> 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()`来这样做。可以看到结果和前面两个例子是一样的。
[*Remove ads*](/account/join/)
## 了解 Python `enumerate()`
在前几节中,您已经看到了何时以及如何使用`enumerate()`的例子。现在您已经掌握了`enumerate()`的实际方面,您可以学习更多关于函数内部如何工作的内容。
为了更好地理解`enumerate()`是如何工作的,您可以用 Python 实现自己的版本。你版本的`enumerate()`有两个要求。它应该:
1. 接受 iterable 和起始计数值作为参数
2. 从 iterable 发回一个包含当前计数值和相关项的元组
编写满足这些规范的函数的一种方法在 [Python 文档](https://docs.python.org/3/library/functions.html#enumerate)中给出:
>>>
```py
>>> 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`的值,并在`sequence`上运行`for`循环。
对于`sequence`中的每个`elem`,你`yield`控制回调用位置,发回`n`和`elem`的当前值。最后,增加`n`为下一次迭代做准备。你可以在这里看到`my_enumerate()`的动作:
>>>
```py
>>> 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')]
```
首先,创建一个四季列表。接下来,您将看到用`seasons`作为`sequence`调用`my_enumerate()`会创建一个生成器对象。这是因为您使用了`yield`关键字将值发送回调用者。
最后,从`my_enumerate()`开始创建两个列表,其中一个列表的起始值保留为默认值`0`,另一个列表的`start`更改为`1`。在这两种情况下,最终都会得到一个元组列表,其中每个元组的第一个元素是计数,第二个元素是来自`seasons`的值。
虽然您可以只用几行 Python 代码实现一个与`enumerate()`等价的函数,但是`enumerate()` [的实际代码是用 C](https://github.com/python/cpython/blob/c8ba47b5518f83b5766fefe6f68557b5033e1d70/Objects/enumobject.c) 编写的。这意味着它是超级快速和高效的。
## 用`enumerate()` 解包参数
当您在一个`for`循环中使用`enumerate()`时,您告诉 Python 使用两个变量,一个用于计数,一个用于值本身。你可以通过使用一个叫做**参数解包**的 Python 概念来做到这一点。
参数解包的思想是一个元组可以根据序列的长度分成几个变量。例如,您可以将两个元素的元组解包为两个变量:
>>>
```py
>>> tuple_2 = (10, "a")
>>> first_elem, second_elem = tuple_2
>>> first_elem
10
>>> second_elem
'a'
```
首先,创建一个包含两个元素的元组,`10`和`"a"`。然后将该元组解包为`first_elem`和`second_elem`,每个元组被赋予一个来自该元组的值。
当您调用`enumerate()`并传递一系列值时Python 返回一个**迭代器**。当您向迭代器请求下一个值时,它会产生一个包含两个元素的元组。元组的第一个元素是计数,第二个元素是您传递的序列中的值:
>>>
```py
>>> 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()`](https://docs.python.org/3/library/functions.html#next) 从`enum_instance`中获取下一个值。`enum_instance`返回的第一个值是一个计数为`0`的元组,来自`values`的第一个元素是`"a"`。
在`enum_instance`上再次调用`next()`产生另一个元组,这一次使用计数`1`和来自`values`、`"b"`的第二个元素。最后,再次调用`next()`会引发`StopIteration`,因为`enum_instance`不再返回任何值。
当在`for`循环中使用 iterable 时Python 会在每次迭代开始时自动调用`next()`,直到引发`StopIteration`为止。Python 将从 iterable 中检索到的值赋给循环变量。
如果 iterable 返回一个元组,那么可以使用参数解包将元组的元素分配给多个变量。这就是你在本教程前面通过使用两个循环变量所做的。
另一次你可能会看到用`for`循环解包参数是用内置的 [`zip()`](https://docs.python.org/3/library/functions.html#zip) ,它允许你同时迭代两个或更多的序列。在每次迭代中, [`zip()`](https://realpython.com/python-zip-function/) 返回一个元组,该元组收集所有传递的序列中的元素:
>>>
```py
>>> 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`循环中,从`first`到`one`,从`second`到`two`,从`third`到`three`分配元素。然后打印这三个值。
您可以通过使用**嵌套**参数解包来组合`zip()`和`enumerate()`:
>>>
```py
>>> 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()`](https://realpython.com/python-zip-function/) 的行为。一种方法使用 [`itertools.count()`](https://realpython.com/python-itertools/#evens-and-odds) ,默认情况下返回从零开始的连续整数。你可以把前面的例子改成使用 [`itertools.count()`](https://docs.python.org/3/library/itertools.html#itertools.count) :
>>>
```py
>>> 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()`调用来生成计数以及循环变量,而不需要对嵌套参数进行解包。
[*Remove ads*](/account/join/)
## 结论
Python 的`enumerate()`允许您在需要来自 iterable 的计数和值时编写 python 式的`for`循环。`enumerate()`的一大优点是它返回一个带有计数器和值的元组,因此您不必自己递增计数器。它还为您提供了更改计数器初始值的选项。
**在本教程中,您学习了如何:**
* 在你的`for`循环中使用 Python 的 **`enumerate()`**
* 在几个**真实世界的例子中应用`enumerate()`**
* 使用**参数解包**从`enumerate()`获取值
* 实现自己的**等价函数**到`enumerate()`
您还看到了在一些真实世界的代码中使用的`enumerate()`,包括在 [CPython](https://realpython.com/cpython-source-code-guide/) 代码库中。现在,您拥有了简化循环并使 Python 代码变得时尚的超能力!
*立即观看**本教程有真实 Python 团队创建的相关视频课程。配合文字教程一起看,加深理解: [**用 Python enumerate()循环**](/courses/looping-with-python-enumerate/)******