循环的问题
我们将通过看一些“陷阱”开始我们的旅程,在我们理解循环如何在 Python 中事情之后,我们将再次看看这些问题并阐明发生了什么。
问题 1:循环两次

假设我们有一个数字列表和一个天生器,天生器会返回这些数字的平方:
>>> numbers = [1, 2, 3, 5, 7]>>> squares = (n2 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 = (n2 for n in numbers)
如果我们讯问 9 是否在 squares 天生器中,Python 将会见告我们 9 在 squares 中。但是如果我们再次讯问相同的问题,Python 会见告我们 9 不在 squares 中。
>>> 9 in squaresTrue>>> 9 in squaresFalse
我们讯问相同的问题两次,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 = (n2 for n in my_set)
因此,任何可以用 for 循环遍历的东西都是可迭代的,序列只是一种可迭代的类型,但是 Python 也有许多其他种类的迭代器。
Python 的 for 循环不该用索引
你可能认为,Python 的 for 循环在底层利用了索引进行循环。在这里我们利用 while 循环和索引手动遍历:
numbers = [1, 2, 3, 5, 7]i = 0while 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 函数要求它们的下一项。如果你将它们通报给 ext,但它们没有下一项了,那么就会引发 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 = Falsewhile not done_looping:try:item = next(iterator)except StopIteration:done_looping = Trueelse:action_to_do(item)
我们只是通过利用 while 循环和迭代看重新定义了 for 循环。
上面的代码基本上定义了 Python 在底层循环的事情办法。如果你理解内置的 iter 和 next函数的遍历循环的事情办法,那么你就会理解 Python 的 for 循环是如何事情的。
事实上,你不仅仅会理解 for 循环在 Python 中是如何事情的,所有形式的遍历一个可迭代工具都是这样事情的。
迭代器协议iterator protocol 是一种很好表示 “在 Python 中遍历迭代器是如何事情的”的办法。它实质上是对 iter 和 ext 函数在 Python 中是如何事情的定义。Python 中所有形式的迭代都是由迭代器协议驱动的。
迭代器协议被 for 循环利用(正如我们已经看到的那样):
for n in numbers:print(n)
多重赋值也利用迭代器协议:
x, y, z = coordinates
星型表达式也是用迭代器协议:
a, b, rest = numbersprint(numbers)
许多内置函数依赖于迭代器协议:
unique_numbers = set(numbers)
在 Python 中任何与迭代器一起事情的东西都可能以某种办法利用迭代器协议。每当你在 Python 中遍历一个可迭代工具时,你将依赖于迭代器协议。
天生器是迭代器
以是你可能会想:迭代器看起来很酷,但它们看起来像一个实现细节,我们作为 Python 的利用者,可能不须要关心它们。
我有见告你:在 Python 中直策应用迭代器是很常见的。
这里的 squares 工具是一个天生器:
>>> numbers = [1, 2, 3]>>> squares = (n2 for n in numbers)
天生器是迭代器,这意味着你可以在天生器上调用 next 来得到它的下一项:
>>> next(squares)1>>> next(squares)4
但是如果你以前用过天生器,你可能也知道可以循环遍历天生器:
>>> squares = (n2 for n in numbers)>>> for n in squares:... print(n)...149
如果你可以在 Python 中循环遍历某些东西,那么它便是可迭代的。
以是天生器是迭代器,但是天生器也是可迭代的,这又是怎么回事呢?
天生器是可迭代的
我再说一遍:Python 中的每一个迭代器都是可迭代的,意味着你可以循环遍历迭代器。
由于迭代器也是可迭代的,以是你可以利用内置 next 函数从可迭代工具中得到迭代器:
>>> numbers = [1, 2, 3]>>> iterator1 = iter(numbers)>>> iterator2 = iter(iterator1)
请记住,当我们在可迭代工具上调用 iter 时,它会给我们返回一个迭代器。
当我们在迭代器上调用 iter 时,它会给我们返回它自己:
>>> iterator1 is iterator2True
迭代器是可迭代的,所有的迭代器都是它们自己的迭代器。
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)[]
你可以把迭代器看作是惰性迭代器,它们是一次性利用,这意味着它们只能循环遍历一次。
正如你不才面的真值表中所看到的,可迭代工具并不总是迭代器,但是迭代器总是可迭代的:
全部的迭代器协议
让我们从 Python 的角度来定义迭代器是如何事情的。
可迭代工具可以被通报给 iter 函数,以便为它们得到迭代器。
迭代器:
可以通报给 next 函数,它将给出下一项,如果没有下一项,那么它将会引发 StopIteration 非常。
可以通报给 iter 函数,它会返回一个自身的迭代器。
这些语句反过来也是精确的:
任何可以在不引发 TypeError 非常的情形下通报给 iter 的东西都是可迭代的
任何可以在不引发 TypeError 非常的情形下通报给 next 的东西都是一个迭代器
当通报给 iter 时,任何返回自身的东西都是一个迭代器
这便是 Python 中的迭代器协议。
迭代器的惰性
迭代器许可我们一起事情,创建惰性可迭代工具,即在我们哀求它们供应下一项之前,它们不做任何事情。由于可以创建惰性迭代器,以是我们可以创建无限长的迭代器。我们可以创建对系统资源比较守旧的迭代器,可以节省我们的内存,节省 CPU 韶光。
迭代器无处不在
你已经在 Python 中看到过许多迭代器,我也提到过天生器是迭代器。Python 的许多内置类型也是迭代器。例如,Python 的 enumerate 和 reversed 工具便是迭代器。
>>> letters = ['a', 'b', 'c']>>> e = enumerate(letters)>>> e<enumerate object at 0x7f112b0e6510>>>> next(e)(0, 'a')
在 Python 3 中,zip, map 和 filter 也是迭代器。
>>> numbers = [1, 2, 3, 5, 7]>>> letters = ['a', 'b', 'c']>>> z = zip(numbers, letters)>>> z<zip object at 0x7f112cc6ce48>>>> next(z)(1, 'a')
Python 中的文件工具也是迭代器。
>>> next(open('hello.txt'))'hello world\n'
在 Python 标准库和第三方库中内置了大量的迭代器。这些迭代器首先惰性迭代器一样,延迟事情直到你要求它们下一项。
创建你自己的迭代器
知道你已经在利用迭代器是很有用的,但是我希望你也知道,你可以创建自己的迭代器和你自己的惰性迭代器。
下面这个类布局了一个迭代器接管一个可迭代的数字,并在循环结束时供应每个数字的平方。
class square_all:def __init__(self, numbers):self.numbers = iter(numbers)def __next__(self):return next(self.numbers) 2def __iter__(self):return self
但是在我们开始对该类的实例进行循环遍历之前,没有任何事情要做。
这里,我们有一个无限长的可迭代工具 count,你可以看到 square_all 接管 count 而不用完备循环遍历这个无限长的迭代:
>>> from itertools import count>>> numbers = count(5)>>> squares = square_all(numbers)>>> next(squares)25>>> next(squares)36
这个迭代器类是有效的,但我们常日不会这样做。常日,当我们想要做一个定制的迭代器时,我们会天生一个天生器函数:
def square_all(numbers):for n in numbers:yield n2
这个天生器函数等价于我们上面所做的类,它的事情事理是一样的。
这种 yield 语句彷佛很神奇,但它非常强大:yield 许可我们在调用 next 函数之间停息天生器函数。yield 语句是将天生器函数与常规函数分离的东西。
另一种实现相同迭代器的方法是利用天生器表达式。
def square_all(numbers):return (n2 for n in numbers)
这和我们的天生器函数确实是一样的,但是它利用的语法看起来像是一个列表推导一样。如果你须要在代码中利用惰性迭代,请考虑迭代器,并考虑利用天生器函数或天生器表达式。
迭代器如何改进你的代码
一旦你已经接管了在代码中利用惰性迭代器的想法,你就会创造有很多可能来创造或创建赞助函数,以此来帮助你循环遍历和处理数据。
惰性求和
这是一个 for 循环,它对 Django queryset 中的所有事情韶光求和:
hours_worked = 0for event in events:if event.is_billable():hours_worked += event.duration
下面是利用天生器表达式进行惰性评估的代码:
billable_times = (event.durationfor event in eventsif event.is_billable())hours_worked = sum(billable_times)
请把稳,我们代码的形状发生了巨大变革。
将我们的打算事情韶光变成一个惰性迭代器许可我们能够命名以前不决名(billable_times)的东西。这大概可我们利用 sum 函数,我们以前不能利用 sum 函数是由于我们乃至没有一个可迭代工具通报给它。迭代器许可你从根本上改变你组织代码的办法。
惰性和冲破循环
这段代码打印出日志文件的前 10 行:
for i, line in enumerate(log_file):if i >= 10:breakprint(line)
这段代码做了同样的事情,但是我们利用的是 itertools.islice 函数来惰性地抓取文件中的前 10 行:
from itertools import islicefirst_ten_lines = islice(log_file, 10)for line in first_ten_lines:print(line)
我们定义的 first_ten_lines 变量是迭代器,同样,利用迭代器许可我们给以前不决名的东西命名(first_ten_lines)。命名事物可以使我们的代码更具描述性,更具可读性。
作为褒奖,我们还肃清了在循环中利用 break 语句的须要,由于 islice 实用函数为我们处理了中断。
你可以在标准库中的 itertools 中找到更多的迭代赞助函数,以及诸如 boltons 和 more-itertools 之类的第三方库。
创建自己的迭代赞助函数
你可以在标准库和第三方库中找到用于循环的赞助函数,但你也可以自己创建!
这段代码列出了序列中连续值之间的差值列表。
current = readings[0]for next_item in readings[1:]:differences.append(next_item - current)current = next_item
请把稳,这段代码中有一个额外的变量,我们每次循环时都要指定它。还要把稳,这段代码只适用于我们可以切片的东西,比如序列。如果 readings 是一个天生器,一个 zip 工具或其他任何类型的迭代器,那么这段代码就会失落败。
让我们编写一个赞助函数来修复代码。
这是一个天生器函数,它为给定的迭代中的每个项目供应了当前项和下一项:
def with_next(iterable):\"大众\"大众\"大众Yield (current, next_item) tuples for each item in iterable.\"大众\"大众\"大众iterator = iter(iterable)current = next(iterator)for next_item in iterator:yield current, next_itemcurrent = next_item
我们从可迭代工具中手动获取一个迭代器,在它上面调用 next 来获取第一项,然后循环遍历迭代器获取后续所有的项目,跟踪后一个项目。这个函数不仅适用于序列,而且适用于任何类型迭代。
这段代码和以前代码是一样的,但是我们利用的是赞助函数而不是手动跟踪 next_item:
differences = []for current, next_item in with_next(readings):differences.append(next_item - current)
请把稳,这段代码不会挂在我们循环周围的 next_item 上,with_next 天生器函数处理跟踪 next_item 的事情。
还要把稳,这段代码已足够紧凑,如果我们乐意,我们乃至可以将方法复制到列表推导中来。
differences = [(next_item - current)for current, next_item in with_next(readings)]
再次回顾循环问题
现在我们准备回到之前看到的那些奇怪的例子并试着找出到底发生了什么。
问题 1:耗尽的迭代器
这里我们有一个天生器工具 squares:
>>> numbers = [1, 2, 3, 5, 7]>>> squares = (n2 for n in numbers)
如果我们把这个天生器通报给 tuple 布局函数,我们将会得到它的一个元组:
>>> numbers = [1, 2, 3, 5, 7]>>> squares = (n2 for n in numbers)>>> tuple(squares)(1, 4, 9, 25, 49)
如果我们试着打算这个天生器中数字的和,利用 sum,我们就会得到 0:
>>> sum(squares)0
这个天生器现在是空的:我们已经把它耗尽了。如果我们试着再次创建一个元组,我们会得到一个空元组:
>>> tuple(squares)()
天生器是迭代器,迭代器是一次性的。它们就像 Hello Kitty Pez 分配器那样不能重新加载。
问题 2:部分花费一个迭代器
再次利用那个天生器工具 squares:
>>> numbers = [1, 2, 3, 5, 7]>>> squares = (n2 for n in numbers)
如果我们讯问 9 是否在 squares 天生器中,我们会得到 True:
>>> 9 in squaresTrue
但是我们再次讯问相同的问题,我们会得到 False:
>>> 9 in squaresFalse
当我们讯问 9 是否在迭代器中时,Python 必须对这个天生器进行循环遍历来找到 9。如果我们在检讨了 9 之后连续循环遍历,我们只会得到末了两个数字,由于我们已经在找到 9 之前花费了这些数字:
>>> numbers = [1, 2, 3, 5, 7]>>> squares = (n2 for n in numbers)>>> 9 in squaresTrue>>> list(squares)[25, 49]
讯问迭代器中是否包含某些东西将会部分地花费迭代器。如果没有循环遍历迭代器,那么是没有办法知道某个东西是否在迭代器中。
问题 3:拆包是迭代
当你在字典上循环时,你会得到键:
>>> counts = {'apples': 2, 'oranges': 1}>>> for key in counts:... print(key)...applesoranges
当你对一个字典进行拆包时,你也会得到键:
>>> x, y = counts>>> x, y('apples', 'oranges')
循环依赖于迭代器协议,可迭代工具拆包也依赖于有迭代器协议。拆包一个字典与在字典上循环遍历是一样的,两者都利用迭代器协议,以是在这两种情形下都得到相同的结果。
回顾
序列是迭代器,但是不是所有的迭代器都是序列。当有人说“迭代器”这个词时,你只能假设他们的意思是“你可以迭代的东西”。不要假设迭代器可以被循环遍历两次、讯问它们的长度或者索引。
迭代器是 Python 中最基本的可迭代形式。如果你想在代码中做一个惰性迭代,请考虑迭代器,并考虑利用天生器函数或天生器表达式。
末了,请记住,Python 中的每一种迭代都依赖于迭代器协议,因此理解迭代器协议是理解 Python 中的循环的关键。
相信现在你已经更深层节制了python中的for循环,如果你还想得到更多python学习资料,可以关注“武汉千锋”微信公众年夜众号!