深入理解Python中的迭代

我们先研究一下Python的for循环,看看它们是如何工作的,原理是什么。

Python的for循环与其它语言的for循环用法不一样。在本文中,我们将深入研究Python的for循环,了解它们是在底层如何工作的,以及背后的原理。

循环陷阱

我们先从一些“陷阱”开始讲起,在了解循环的工作原理之后,我们再回头看看这些陷阱并解释发生了什么。

陷阱1:循环两次

假设我们有一个数字列表和一个生成器,生成器会返回这些数字的平方:

我们可以将生成器传递给tuple构造器,使其变为一个元组:

如果继续使用该生成器并将其传递给sum函数,我们可能会期望得到这些数字的总和,即88。

但是我们得到了0。

陷阱2:检查是否包含

继续使用上面的数字列表和生成器:

如果查询9是否在squares生成器中,Python会告诉我们9在squares中。但如果再次进行同样的查询,结果是9不在squares中。

我们进行了两次同样的查询,Python给了我们不同的答案。

陷阱3:解包

这个字典有两个键值对:

让我们用多重赋值解包这个字典:

你可能期望在解包此字典时,我们将获得键值对,或者会有错误提示。

但是解包字典不会引发错误,也不会返回键值对。当你解包字典时,你只会获得键:

我们先学习一些背后的逻辑,再回头来看这些陷阱。

复习Python的for循环

Python没有传统的for循环。为了解释我的意思,让我们看看另一种编程语言中的for循环。

这是一个用JavaScript编写的传统C风格的for循环:

JavaScript,C,C ++,Java,PHP和其它许多编程语言都有这种for循环。但Python确实没有。

Python没有传统C风格的for循环。在Python中有一些我们称之为for循环的东西,但它的工作方式类似于foreach循环。

这是Python的for循环风格:

与传统的C风格for循环不同,Python的for循环没有索引变量。没有索引初始化,边界检查或索引递增。 Python 的 for循环完成了遍历numbers列表的所有工作。

因此,虽然Python中有for循环,但不是传统的C风格for循环。C风格的for循环工作方式与Python风格的大不一样。

定义:可迭代对象和序列

我们已经讲过Python中无索引的for循环,那么让我们来看看一些定义。

可迭代对象是任何可以用for循环遍历的东西。可迭代对象可以被循环遍历,任何可以被循环遍历的东西都是可迭代对象。

序列是一种很常见的可迭代对象。列表,元组和字符串都是序列。

序列是有固定特征集的可迭代对象。它们可以从0开始索引,并以序列长度减1的值结束,它们有长度,可以分割。列表,元组,字符串和所有其它序列以这种方式工作。

Python中的很多东西都是可迭代对象,但并非所有的可迭代对象都是序列。集合,字典,文件和生成器都是可迭代对象,但这些都不是序列。

所以任何可以用for循环遍历的东西都是可迭代对象,序列是一种可迭代对象,但Python也有许多其它类型的可迭代对象。

Python的for循环不使用索引

你可能会认为Python的for循环在底层使用索引来循环。这里我们使用while循环和索引对一个可迭代对象进行手动循环遍历:

这适用于列表,但它不会对所有东西起作用。这种循环方式仅适用于序列

如果尝试用索引手动循环遍历一个集合,会抛出一个错误:

集合不是序列,因此它们不支持索引。

我们不能用索引手动循环Python中的所有可迭代对象,对于不是序列的可迭代对象不起作用。

迭代器驱动for循环

所以我们已经看到Python的for循环在底层不使用索引。Python的for循环使用迭代器。

迭代器驱动可迭代对象。你可以从任何可迭代对象中获得一个迭代器。你可以使用迭代器手动循环遍历可迭代对象。

我们来看看它是如何工作的。

这里有三个可迭代对象:一个集合,一个元组和一个字符串。

我们可以使用Python的内置iter函数向每个可迭代对象要一个迭代器。将一个可迭代对象传递给iter函数就返回一个迭代器,无论我们操作什么类型的可迭代对象。

一旦我们有了迭代器,我们就可以用内置的next函数来获取它的下一个元素。

迭代器是有状态的,这意味着一旦你使用了一个元素,它就消失了。

如果你想从迭代器请求next元素,但已经没有更多元素了,就会发生StopIteration异常:

所以你可以从每个可迭代对象中获得一个迭代器。迭代器唯一能做的事情就是使用next函数获取它的下一个元素。如果使用next函数但是没有下一个元素,则会抛出StopIteration异常。

你可以将迭代器视为无法重新加载的Pez分配器。你可以把Pez移除,但是一旦Pez被移除它就不能放回去,一旦分配器空了,它就没用了。

不用for语句来循环

现在我们已经了解了迭代器以及iter和next函数,我们将尝试在不使用for循环的情况下来手动循环遍历可迭代对象。

我们尝试将此for循环转换为while循环来实现:

为了做到这点,我们需要:

1.从给定的可迭代对象中获取迭代器

2.反复从迭代器获取下一个元素

3.如果我们成功获得下一个元素,则执行for循环的主体

4.如果我们在获取下一个元素时遇到StopIteration异常,那么就停止循环

我们通过while循环和迭代器实现了for循环的功能。

上面的代码基本定义了Python中循环的工作方式。如果你理解内置iter和next函数遍历循环的工作方式,你就会理解Python的for循环是如何工作的。

事实上,你不仅仅会理解 for 循环在 Python 中是如何工作的,可迭代对象所有形式的循环都以这种方式工作。

迭代器协议就像是在说“如何在Python中循环遍历可迭代对象”。它本质上是定义iter和next函数在Python中的工作方式。Python中所有形式的迭代都是由迭代器协议驱动的。

for循环使用迭代器协议(正如我们已经看到的那样):

多重赋值也使用迭代器协议:

星形表达式使用迭代器协议:

许多内置函数依赖于迭代器协议:

Python中与可迭代对象一起使用的任何东西都以某种方式使用迭代器协议。无论何时在Python中循环遍历一个可迭代对象,都依赖于迭代器协议。

生成器是迭代器

所以你可能会想:迭代器看起来很酷,但它们看起来像一个实现细节,我们作为 Python 的使用者,可能不需要关心它们。

我有消息告诉你:在 Python 中直接使用迭代器是很常见的。

这里的 squares 对象是一个生成器:

生成器也是迭代器,这意味着你可以在生成器上调用next函数来获取它的下一个元素:

如果你之前用过生成器,你就知道也可以循环遍历生成器:

如果你可以在Python中循环遍历某个东西,那它就是可迭代对象。

所以生成器是迭代器,但生成器也是可迭代对象。这里发生了什么?

我欺骗了你

之前我解释迭代器如何工作时,我跳过了某些重要细节。

迭代器是可迭代对象。

我再说一遍:Python中的每个迭代器也是一个可迭代对象,这意味着你可以遍历迭代器。

因为迭代器也是可迭代对象,所以你可以使用内置的iter函数从迭代器中获取迭代器:

记住,当在可迭代对象上调用iter函数时,会返回迭代器。

当在迭代器上调用iter函数时,返回迭代器本身:

迭代器是可迭代对象,所有迭代器都是它们自己的迭代器。

困惑了吗?

让我们回顾一下这些措辞:

一个可迭代对象是你能够迭代的东西

一个迭代器是对一个可迭代对象进行迭代的工具

另外,在Python中,迭代器也是可迭代对象,它们充当自己的迭代器。

所以迭代器是可迭代对象,但它们没有可迭代对象所具有的各种特性。

迭代器没有长度,也无法被索引:

从Python程序员的角度来看,迭代器唯一有用的地方是将其传递给内置的 next 函数,或者对其进行循环遍历:

如果我们第二次循环遍历迭代器,我们将一无所获:

你可以将迭代器视为一次性使用的惰性的可迭代对象,这意味着它们只能遍历循环一次。

正如你在下面的真值表中所看到的,可迭代对象并不总是迭代器,但迭代器总是可迭代对象:

完整的迭代器协议

让我们从Python的角度定义迭代器的工作方式。

可迭代对象可以被传递给iter函数以获取它们的迭代器。

迭代器:

可以传递给next函数,返回下一个元素,如果没有更多元素,抛出StopIteration异常

可以传递给iter函数并返回自身

这些语句反过来也是正确的::

任何可以在不引发TypeError异常的情况下传递给iter函数的东西都是可迭代对象

任何可以在不引发TypeError异常的情况下传递给next函数的东西都是一个迭代器

任何可以传递给iter函数且返回自身的东西都是迭代器

这就是Python中的迭代器协议。

迭代器是惰性的

迭代器可以创建惰性的可迭代对象,在我们要求获取下一个元素之前不做任何工作。因为可以创建惰性的可迭代对象,所以我们可以创建无限长的可迭代对象。我们可以创建节省系统资源的可迭代对象,这样既可以节省内存,又可以节省CPU运算时间。

迭代器无处不在

你已经在Python中看到了很多迭代器。我已经提到过生成器是迭代器。许多Python的内置类也是迭代器。例如,Python的enumerate和reversed对象是迭代器。

在Python 3中,zip,map和filter对象也是迭代器。

Python中的文件对象也是迭代器。

Python标准库和第三方库中内置了大量迭代器。这些迭代器都像惰性的可迭代对象一样,延迟工作直到你请求它们的下一个元素。

创建自己的迭代器

你应该知道你在使用迭代器,但我希望你也知道你可以创建自己的迭代器和惰性的可迭代对象。

这个类构造了一个迭代器,它接受一组可迭代的数字,并在循环时提供每个数字的平方。

但是在我们开始对该类的实例进行循环遍历之前,不会进行任何运算或操作。

这里我们有一个无限长的可迭代对象count,你可以看到square_all接受count而不完全循环遍历这个无限长的可迭代对象:

这个迭代器类是有效的,但我们通常不会这样做。通常,当我们想要做一个定制的迭代器时,我们会定义一个生成器函数:

这个生成器函数等同于我们上面创建的类,它的工作原理是一样的。

这种yield语句似乎很神奇,但它非常强大:yield允许我们在调用next函数之间暂停生成器函数。 yield语句是将生成器函数与常规函数分离的东西。

另一种实现相同迭代器功能的方法是使用生成器表达式。

这与生成器函数的作用相同,但它使用的语法看起来像列表推导。如果你要在代码中使用惰性的可迭代对象,请考虑使用迭代器,并定义一个生成器函数或生成器表达式。

迭代器如何改进代码

一旦你有了在代码中使用惰性可迭代对象的想法,你会发现可以用很多方法创建辅助函数,帮助你循环遍历可迭代对象和处理数据。

惰性与求和

这是一个for循环,它对Django查询集中的所有可计费时间求和:

下面的代码是使用生成器表达式进行惰性计算,跟上面的代码结果一样:

请注意,我们的代码形状发生了巨大变化。

将可计费时间变成一个惰性的可迭代对象,这样可以命名以前未命名的东西(billable_times)。也可以使用sum函数。之前不能使用sum函数是因为没有可迭代对象传递给它。迭代器允许你从根本上改变组织代码的方式。

惰性与跳出循环

这段代码打印出日志文件的前10行:

下面这段代码做了同样的事情,但在循环中使用了itertools.islice函数来惰性抓取文件的前10行:

我们创建的first_ten_lines变量是一个迭代器。同样,使用迭代器可以为之前未命名的东西(first_ten_lines)命名。命名事物可以使我们的代码更具描述性和可读性。

另外,我们还在循环中删除了break语句,因为islice工具进行了中断。

你可以在标准库中的itertools以及第三方库的boltons和more-itertools中找到更多的迭代辅助函数。

创建自己的迭代辅助函数

你可以在标准库和第三方库中找到用于循环的辅助函数,但你也可以创建自己的函数!

这段代码列出了序列中连续值之间的差值列表。

请注意,此代码有一个额外的变量,我们需要在每次循环时赋值于该变量。另请注意,此代码仅适用于可以分割的对象,例如序列。如果readings是一个生成器,一个zip对象或任何其他类型的迭代器,则此代码将失效。

让我们编写一个辅助函数来修复代码。

下面是一个生成器函数,它为给定的可迭代对象中的每个元素提供当前元素及下一元素:

我们从可迭代对象中手动获取一个迭代器,在它上面调用next函数来获取第一个元素,再循环遍历迭代器以获取所有后续元素,跟踪后一个元素。此函数不仅适用于序列,还适用于任何类型的可迭代对象。

这与之前的代码作用相同,但使用的是辅助函数,而不是手动跟踪next_item:

还要注意,这段代码已足够紧凑,如果我们愿意,我们甚至可以将方法复制到列表推导中来。

再次回顾循环陷阱

现在我们回到之前看到的那些奇怪的例子,看看到底发生了什么。

陷阱1:耗尽迭代器

这里我们有一个生成器squares:

如果我们将这个生成器传递给tuple构造函数,我们将得到一个元组:

如果用sum计算这个生成器中数字的总和,我们将得到0:

这个生成器现在是空的:因为我们已经把它耗尽了。如果我们试着再次创建一个元组,我们会得到一个空元组:

生成器是迭代器。迭代器是一次性的可迭代对象。它们就像 Hello Kitty Pez 分配器那样不能重新加载。

陷阱2:部分消耗一个迭代器

再次使用那个生成器对象 squares:

如果我们查询 9是否在 squares生成器中,我们会得到 True:

但是我们再次查询相同的问题,我们会得到 False:

当我们查询9是否在这个生成器中时,Python必须遍历这个生成器才能找到9.如果我们在查询到9后继续循环它,我们只会得到最后两个数字,因为我们已经消耗了前面的数字:

询问迭代器中是否包含某些内容会消耗迭代器中部分元素。如果没有循环遍历迭代器,那么是没有办法知道某个东西是否在迭代器中。

陷阱3:解包是迭代

当你遍历字典时,你会获得键:

解包字典也可以获得键:

循环依赖于迭代器协议。可迭代对象的解包也依赖于迭代器协议。解包字典与循环遍历字典是一样的。两者都使用迭代器协议,因此两种情况下会得到相同的结果。

回顾和相关资源

序列是可迭代对象,但不是所有的可迭代对象都是序列。当有人说“可迭代对象”这个词时,你只能假设他们的意思是“你可以迭代的东西”。不要假设可迭代对象可以被循环遍历两次,询问它们的长度或索引。

在Python中迭代器是最基本的可迭代对象。如果你想在代码中使用惰性的可迭代对象,请考虑迭代器并考虑使用生成器函数或生成器表达式。

最后,请记住,Python中的每种类型的迭代都依赖于迭代器协议,因此理解迭代器协议是理解 Python 中的循环的关键。

以下是关文章和视频:

《像行家一样使用循环》(链接:https://nedbatchelder.com/text/iter.html),PyCon2013主题演讲。

《更好地使用循环》(链接:https://www.youtube.com/watch?v=V2PkkMS2Ack),本文就是基于这个演讲整理出来的。

《 迭代器协议:循环如何工作》(链接:http://treyhunner.com/2016/12/python-iterator-protocol-how-for-loops-work/),我写的一篇关于迭代器协议的简短文章。

《深入理解推导式》(链接:https://www.youtube.com/watch?v=5_cJIcgM7rw),关于推导式和生成器表达式的演讲。

《Python:Range不是迭代器》,(链接:http://treyhunner.com/2018/02/python-range-is-not-an-iterator/)我写的关于range和迭代器的文章。

《像专家一样使用循环》(链接:https://www.youtube.com/watch?v=u8g9scXeAcI),PyCon 2017主题演讲。

本文基于作者去年在澳大利亚DjangoCon大会,PyGotham会议和North Bay Python会议上发表的关于深入理解循环的演讲。想了解更多内容,请参加将于2018年5月9日至17日在俄亥俄州哥伦布市举行的PYCON大会。

英文原文:https://opensource.com/article/18/3/loop-better-deeper-look-iteration-python

译者:钱利鹏

  • 发表于:
  • 原文链接:https://kuaibao.qq.com/s/20180728A09KVQ00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券