深入探讨 Python 的
for
循环来看看它们在底层如何工作,以及为什么它们会按照它们的方式工作。
Python 的 for
循环不会像其他语言中的 for
循环那样工作。在这篇文章中,我们将深入探讨 Python 的 for
循环来看看它们在底层如何工作,以及为什么它们会按照它们的方式工作。
循环的问题
我们将通过看一些“陷阱”开始我们的旅程,在我们了解循环如何在 Python 中工作之后,我们将再次看看这些问题并解释发生了什么。
问题 1:循环两次
假设我们有一个数字列表和一个生成器,生成器会返回这些数字的平方:
>>> numbers = [1, 2, 3, 5, 7]
>>> squares = (n**2 for n in numbers)
我们可以将生成器对象传递给 tuple
构造器,从而使其变为一个元组:
>>> tuple(squares)
(1, 4, 9, 25, 49)
如果我们使用相同的生成器对象并将其传给 sum
函数,我们可能会期望得到这些数的和,即 88
。
>>> sum(squares)
0
但是我们得到了 0
。
问题 2:包含的检查
让我们使用相同的数字列表和相同的生成器对象:
>>> numbers = [1, 2, 3, 5, 7]
>>> squares = (n**2 for n in numbers)
如果我们询问 9
是否在 squares
生成器中,Python 将会告诉我们 9 在 squares
中。但是如果我们再次询问相同的问题,Python 会告诉我们 9 不在 squares
中。
>>> 9 in squares
True
>>> 9 in squares
False
我们询问相同的问题两次,Python 给了两个不同的答案。
问题 3 :拆包
这个字典有两个键值对:
>>> counts = {'apples': 2, 'oranges': 1}
让我们使用多个变量来对这个字典进行拆包:
>>> x, y = counts
你可能会期望当我们对这个字典进行拆包时,我们会得到键值对或者得到一个错误。
但是解包字典不会引发错误,也不会返回键值对。当你解包一个字典时,你会得到键:
>>> x
'apples'
回顾:Python 的 for 循环
在我们了解一些关于这些 Python 片段的逻辑之后,我们将回到这些问题。
Python 没有传统的 for
循环。为了解释我的意思,让我们看一看另一种编程语言的 for
循环。
这是一种传统 C 风格的 for
循环,用 JavaScript 编写:
let numbers = [1, 2, 3, 5, 7];
for (let i = 0; i < numbers.length; i += 1) {
print(numbers[i])
}
JavaScript、 C、 C++、 Java、 PHP 和一大堆其他编程语言都有这种风格的 for
循环,但是 Python 确实没有。
Python 确实没有 传统 C 风格的 for
循环。在 Python 中确实有一些我们称之为 for
循环的东西,但是它的工作方式类似于 foreach 循环。
这是 Python 的 for
循环的风格:
numbers = [1, 2, 3, 5, 7]
for n in numbers:
print(n)
与传统 C 风格的 for
循环不同,Python 的 for
循环没有索引变量,没有索引变量初始化,边界检查,或者索引递增。Python 的 for
循环完成了对我们的 numbers
列表进行遍历的所有工作。
因此,当我们在 Python 中确实有 for
循环时,我们没有传统 C 风格的 for
循环。我们称之为 for 循环的东西的工作机制与之相比有很大的不同。
定义:可迭代和序列
既然我们已经解决了 Python 世界中无索引的 for
循环,那么让我们在此之外来看一些定义。
可迭代是任何你可以用 Python 中的 for
循环遍历的东西。可迭代意味着可以遍历,任何可以遍历的东西都是可迭代的。
for item in some_iterable:
print(item)
序列是一种非常常见的可迭代类型,列表,元组和字符串都是序列。
>>> numbers = [1, 2, 3, 5, 7]
>>> coordinates = (4, 5, 7)
>>> words = "hello there"
序列是可迭代的,它有一些特定的特征集。它们可以从 0
开始索引,以小于序列的长度结束,它们有一个长度并且它们可以被切分。列表,元组,字符串和其他所有序列都是这样工作的。
>>> numbers[0]
1
>>> coordinates[2]
7
>>> words[4]
'o'
Python 中很多东西都是可迭代的,但不是所有可迭代的东西都是序列。集合、字典、文件和生成器都是可迭代的,但是它们都不是序列。
>>> my_set = {1, 2, 3}
>>> my_dict = {'k1': 'v1', 'k2': 'v2'}
>>> my_file = open('some_file.txt')
>>> squares = (n**2 for n in my_set)
因此,任何可以用 for
循环遍历的东西都是可迭代的,序列只是一种可迭代的类型,但是 Python 也有许多其他种类的迭代器。
Python 的 for 循环不使用索引
你可能认为,Python 的 for
循环在底层使用了索引进行循环。在这里我们使用 while
循环和索引手动遍历:
numbers = [1, 2, 3, 5, 7]
i = 0
while i < len(numbers):
print(numbers[i])
i += 1
这适用于列表,但它不会对所有东西都起作用。这种循环方式只适用于序列。
如果我们尝试用索引去手动遍历一个集合,我们会得到一个错误:
>>> fruits = {'lemon', 'apple', 'orange', 'watermelon'}
>>> i = 0
>>> while i < len(fruits):
... print(fruits[i])
... i += 1
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
TypeError: 'set' object does not support indexing
集合不是序列,所以它们不支持索引。
我们不能使用索引手动对 Python 中的每一个迭代对象进行遍历。对于那些不是序列的迭代器来说,这是行不通的。
迭代器驱动 for 循环
因此,我们已经看到,Python 的 for
循环在底层不使用索引。相反,Python 的 for
循环使用迭代器。
迭代器就是可以驱动可迭代对象的东西。你可以从任何可迭代对象中获得迭代器,你也可以使用迭代器来手动对它的迭代进行遍历。
让我们来看看它是如何工作的。
这里有三个可迭代对象:一个集合,一个元组和一个字符串。
>>> numbers = {1, 2, 3, 5, 7}
>>> coordinates = (4, 5, 7)
>>> words = "hello there"
我们可以使用 Python 的内置 iter
函数来访问这些迭代器,将一个迭代器传递给 iter
函数总会给我们返回一个迭代器,无论我们正在使用哪种类型的迭代器。
>>> iter(numbers)
<set_iterator object at 0x7f2b9271c860>
>>> iter(coordinates)
<tuple_iterator object at 0x7f2b9271ce80>
>>> iter(words)
<str_iterator object at 0x7f2b9271c860>
一旦我们有了迭代器,我们可以做的事情就是通过将它传递给内置的 next
函数来获取它的下一项。
>>> numbers = [1, 2, 3]
>>> my_iterator = iter(numbers)
>>> next(my_iterator)
1
>>> next(my_iterator)
2
迭代器是有状态的,这意味着一旦你从它们中消耗了一项,它就消失了。
如果你从迭代器中请求 next
项,但是其中没有更多的项了,你将得到一个 StopIteration
异常:
>>> next(my_iterator)
3
>>> next(my_iterator)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
所以你可以从每个迭代中获得一个迭代器,迭代器唯一能做的事情就是用 next
函数请求它们的下一项。如果你将它们传递给 next
,但它们没有下一项了,那么就会引发 StopIteration
异常。
你可以将迭代器想象成 Pez 分配器(LCTT 译注:Pez 是一个结合玩具的独特复合式糖果),不能重新分配。你可以把 Pez 拿出去,但是一旦 Pez 被移走,它就不能被放回去,一旦分配器空了,它就没用了。
没有 for 的循环
既然我们已经了解了迭代器和 iter
以及 next
函数,我们将尝试在不使用 for
循环的情况下手动遍历迭代器。
我们将通过尝试将这个 for
循环变为 while
循环:
def funky_for_loop(iterable, action_to_do):
for item in iterable:
action_to_do(item)
为了做到这点,我们需要:
- 从给定的可迭代对象中获得迭代器
- 反复从迭代器中获得下一项
- 如果我们成功获得下一项,就执行
for
循环的主体 - 如果我们在获得下一项时得到了一个
StopIteration
异常,那么就停止循环
def funky_for_loop(iterable, action_to_do):
iterator = iter(iterable)
done_looping = False
while not done_looping:
try:
item = next(iterator)
except StopIteration:
done_looping = True
else:
action_to_do(item)
我们只是通过使用 while
循环和迭代器重新定义了 for
循环。
上面的代码基本上定义了 Python 在底层循环的工作方式。如果你理解内置的 iter
和 next
函数的遍历循环的工作方式,那么你就会理解 Python 的 for
循环是如何工作的。
事实上,你不仅仅会理解 for
循环在 Python 中是如何工作的,所有形式的遍历一个可迭代对象都是这样工作的。
迭代器协议 是一种很好表示 “在 Python 中遍历迭代器是如何工作的”的方式。它本质上是对 iter
和 next
函数在 Python 中是如何工作的定义。Python 中所有形式的迭代都是由迭代器协议驱动的。
迭代器协议被 for
循环使用(正如我们已经看到的那样):
for n in numbers:
print(n)
多重赋值也使用迭代器协议:
x, y, z = coordinates
星型表达式也是用迭代器协议:
a, b, *rest = numbers
print(*numbers)
许多内置函数依赖于迭代器协议:
unique_numbers = set(numbers)
在 Python 中任何与迭代器一起工作的东西都可能以某种方式使用迭代器协议。每当你在 Python 中遍历一个可迭代对象时,你将依赖于迭代器协议。
生成器是迭代器
所以你可能会想:迭代器看起来很酷,但它们看起来像一个实现细节,我们作为 Python 的使用者,可能不需要关心它们。
我有消息告诉你:在 Python 中直接使用迭代器是很常见的。
这里的 squares
对象是一个生成器:
>>> numbers = [1, 2, 3]
>>> squares = (n**2 for n in numbers)
生成器是迭代器,这意味着你可以在生成器上调用 next
来获得它的下一项:
>>> next(squares)
1
>>> next(squares)
4
但是如果你以前用过生成器,你可能也知道可以循环遍历生成器:
>>> squares = (n**2 for n in numbers)
>>> for n in squares:
... print(n)
...
1
4
9
如果你可以在 Python 中循环遍历某些东西,那么它就是可迭代的。
所以生成器是迭代器,但是生成器也是可迭代的,这又是怎么回事呢?
我欺骗了你
所以在我之前解释迭代器如何工作时,我跳过了它们的某些重要的细节。
生成器是可迭代的
我再说一遍:Python 中的每一个迭代器都是可迭代的,意味着你可以循环遍历迭代器。
因为迭代器也是可迭代的,所以你可以使用内置 next
函数从可迭代对象中获得迭代器:
>>> numbers = [1, 2, 3]
>>> iterator1 = iter(numbers)
>>> iterator2 = iter(iterator1)
请记住,当我们在可迭代对象上调用 iter
时,它会给我们返回一个迭代器。
当我们在迭代器上调用 iter
时,它会给我们返回它自己:
>>> iterator1 is iterator2
True
迭代器是可迭代的,所有的迭代器都是它们自己的迭代器。
def is_iterator(iterable):
return iter(iterable) is iterable
迷惑了吗?
让我们回顾一些这些措辞。
- 一个可迭代对象是你可以迭代的东西
- 一个迭代对象器是一种实际上遍历可迭代对象的代理
此外,在 Python 中迭代器也是可迭代的,它们充当它们自己的迭代器。
所以迭代器是可迭代的,但是它们没有一些可迭代对象拥有的各种特性。
迭代器没有长度,它们不能被索引:
>>> numbers = [1, 2, 3, 5, 7]
>>> iterator = iter(numbers)
>>> len(iterator)
TypeError: object of type 'list_iterator' has no len()
>>> iterator[0]
TypeError: 'list_iterator' object is not subscriptable
从我们作为 Python 程序员的角度来看,你可以使用迭代器来做的唯一有用的事情是将其传递给内置的 next
函数,或者对其进行循环遍历:
>>> next(iterator)
1
>>> list(iterator)
[2, 3, 5, 7]
如果我们第二次循环遍历迭代器,我们将一无所获:
>>> list(iterator)
[]
你可以把迭代器看作是惰性迭代器,它们是一次性使用,这意味着它们只能循环遍历一次。
正如你在下面的真值表中所看到的,可迭代对象并不总是迭代器,但是迭代器总是可迭代的:
对象 | 可迭代? | 迭代器? |
---|
via: https://opensource.com/article/18/3/loop-better-deeper-look-iteration-python
作者:Trey Hunner 译者:MjSeven 校对:wxy
发表回复