Python进阶——什么是迭代器?

Python进阶——什么是迭代器?

在 Python 开发中,我们经常听到有关「容器」、「迭代器」、「可迭代对象」、「生成器」的概念。

我们经常把这些概念搞混淆,它们之间有哪些联系和区别呢?

这篇文章,我们就来看一下它们之间的关系。

容器

首先,我们先来看一下容器是如何定义的?

简单来说,容器就是存储某些元素的统称,它最大的特性就是判断一个元素是否在这个容器内。

怎么理解这句话?

很简单,在 Python 中,我们通常使用 in 或 not in 来判断一个元素存在/不存在于一个容器内。

例如下面这个例子:

print('x'in'xyz')# Trueprint('a'notin'xyz')# Trueprint(1in[1,2,3])# Trueprint(2notin(1,2,3))# Falseprint('x'notin{'a','b','c'})# Trueprint('a'in{'a':1,'b':2})# True

在这个例子中,我们可以看到 str、list、tuple、set、dict 都可以通过 in 或 not in 来判断一个元素是否在存在/不存在这个实例中,所以这些类型我们都可以称作「容器」。

那为什么这些「容器」可以使用 in 或 not in 来判断呢?

这是因为它们都实现了 __contains__ 方法。

如果我们也想自定义一个容器,只需像下面这样,在类中定义 __contains__ 方法就可以了:

classA:def__init__(self):self.items=[1,2]def__contains__(self,item):returniteminself.itemsa=A()print(1ina)# Trueprint(2ina)# Trueprint(3ina)# False

在这个例子中,类 A 定义了 __contains__ 方法,所以我们就可以使用 1 in a 的方式去判断这个元素是否在 A 这个容器内。

换句话说,一个类只要实现了 __contains__ 方法,那么它就是一个「容器」。

我们在开发时,除了使用 in 判断元素是否在容器内之外,另外一个常用的功能是:输出容器内的所有元素。

例如执行 for x in [1, 2, 3],就可以迭代出容器内的所有元素。

那使用这种方式输出元素,是如何实现的?这就跟「迭代器」有关了。

迭代器

一个对象要想使用 for 的方式迭代出容器内的所有数据,这就需要这个类实现「迭代器协议」。

也就是说,一个类如果实现了「迭代器协议」,就可以称之为「迭代器」。

什么是「迭代器协议」呢?

在 Python 中,实现迭代器协议就是实现以下 2 个方法:

__iter__:这个方法返回对象本身,即 self

__next__:这个方法每次返回迭代的值,在没有可迭代元素时,抛出 StopIteration 异常

下面我们来看一个实现迭代器协议的例子:

# coding: utf8classA:"""A 实现了迭代器协议 它的实例就是一个迭代器"""def__init__(self,n):self.idx=0self.n=ndef__iter__(self):print('__iter__')returnselfdef__next__(self):ifself.idx<self.n:val=self.idxself.idx+=1returnvalelse:raiseStopIteration()# 迭代元素a=A(3)foriina:print(i)# 再次迭代 没有元素输出 因为迭代器只能迭代一次foriina:print(i)# __iter__# 0# 1# 2# __iter__

在这个例子中,我们定义了一个类 A,它内部实现了 __iter__ 和 __next__ 方法。

其中 __iter__ 方法返回了 self,__next__ 方法实现了具体的迭代细节。

然后执行 a = A(3),在执行 for i in a 时,我们看到调用了 __iter__ 方法,然后依次输出 __next__ 中的元素。

其实在执行 for 循环时,实际执行流程是这样的:

for i in a 相当于执行 iter(a)

每次迭代时会执行一次 __next__ 方法,返回一个值

如果没有可迭代的数据,抛出 StopIteration 异常,for 会停止迭代

但是请注意,当我们迭代完 for i in a 时,如果再次执行迭代,将不会有任何数据输出。

如果我们想每次执行都能迭代元素,只需每次迭代一个新对象即可:

# 每次都迭代一个对象foriinA(3):print(i)

可迭代对象

明白了「迭代器」是如何执行的,我们接着来看什么是「可迭代对象」?

这是什么意思?难道一个类是「迭代器」,那么它的实例不是一个「可迭代对象」吗? 它们之间又有什么区别?

其实,但凡是可以返回一个「迭代器」的对象,都可以称之为「可迭代对象」。

换句话说:__iter__ 方法返回一个迭代器,那么这个对象就是「可迭代对象」。

听起来不太好理解,我们来看一个例子。

classA:# A是迭代器 因为它实现了 __iter__ 和__next__方法def__init__(self,n):self.idx=0self.n=ndef__iter__(self):returnselfdef__next__(self):ifself.idx<self.n:val=self.idxself.idx+=1returnvalelse:raiseStopIteration()classB:# B不是迭代器 但B的实例是一个可迭代对象# 因为它只实现了 __iter__# __iter__返回了A的实例 迭代细节交给了Adef__init__(self,n):self.n=ndef__iter__(self):returnA(self.n)# a是一个迭代器 同时也是一个可迭代对象a=A(3)foriina:print(i)# <__main__.A object at 0x10eb95550>print(iter(a))# b不是迭代器 但它是可迭代对象 因为它把迭代细节交给了Ab=B(3)foriinb:print(i)# <__main__.A object at 0x10eb95450>print(iter(b))

仔细看这个例子,我们定义了 2 个类 A 和 B,A 实现了 __iter__ 和 __next__ 方法。

而 B 只实现了 __iter__,并没有实现 __next__,而且它的 __iter__ 返回值是一个 A 的实例。

对于 A 来说:

A 是一个「迭代器」,因为其实现了迭代器协议 __iter__ 和 __next__

同时 A 的 __iter__ 方法返回了实例本身 self,也就是说返回了一个迭代器,所以 A 的实例 a 也是一个「可迭代对象」

对于B 来说:

B 不是一个「迭代器」,因为它只了实现 __iter__,没有实现 __next__

由于 B 的 __iter__ 返回了 A 的实例,而 A 是一个迭代器,所以 B 的实例 b 是一个「可迭代对象」,换句话说,B 把迭代细节交给了 A

总之,一个类的迭代细节,是可以交给另一个类的,就像这个例子的 B 这样,所以 B 的实例只能是「可迭代对象」,而不是「迭代器」。

其实,这种情况我们见的非常多,我们使用最多的 list、tuple、set、dict 类型,都只是「可迭代对象」,但不是「迭代器」,因为它们都是把迭代细节交给了另外一个类,这个类才是真正的迭代器。

看下面这个例子,你就能明白这两者之间的差别了。

# list 是可迭代对象>>>l=[1,2]# list 的迭代器是 list_iterator>>>iter(l)<list_iteratorobjectat0x1009c1c18># 执行的是 list_iterator 的 __next__>>>iter(l).__next__()>>>1# tuple 是可迭代对象>>>t=('a','b')# tuple 的迭代器是 tuple_iterator>>>iter(t)<tuple_iteratorobjectat0x1009c1b00># 执行的是 tuple_iterator 的 __next__>>>iter(t).__next__()>>>a# set 是可迭代对象>>>s={1,2}# set 的迭代器是 set_iterator>>>iter(s)<set_iteratorobjectat0x1009c70d8># 执行的是 set_iterator 的 __next__>>>iter(s).__next__()>>>1# dict 是可迭代对象>>>d={'a':1,'b':2}# dict 的迭代器是 dict_keyiterator>>>iter(d)# 执行的是 dict_keyiterator 的 __next__<dict_keyiteratorobjectat0x1009c34f8>>>>iter(d).next()>>>a

以 list 类型为例,我们先定义 l = [1, 2],然后执行 iter(l) 得到 list 类型的迭代器是 list_iterator,也就是说在迭代 list 时,其实执行的是 list_iterator 的 __next__,list 把具体的迭代细节,交给了 list_iterator。

所以 list 是一个可迭代对象,但它不是迭代器。其他类型 tuple、set、dict 也是同样的道理。

由此我们可以得出一个结论:迭代器一定是个可迭代对象,但可迭代对象不一定是迭代器。

生成器

我们再来看什么是「生成器」?

其实,「生成器」是一个特殊的「迭代器」,并且它也是一个「可迭代对象」。

有 2 种方式可以创建一个生成器:

生成器表达式

生成器函数

用生成器表达式创建一个生成器的例子如下:

# 创建一个生成器 类型是 generator>>>g=(iforiinrange(5))>>>g<generatorobject<genexpr>at0x101334f50># 生成器就是一个迭代器>>>iter(g)<generatorobject<genexpr>at0x101334f50># 生成器也是一个可迭代对象>>>foriing:...print(i)# 0 1 2 3 4

注意看这个例子,我们使用 g = (i for i in range(5)) 创建了一个生成器,它的类型是 generator,同时调用 iter(g) 可以得知 __iter__ 返回的是实例本身,即生成器也是一个迭代器,并且它也是一个可迭代对象。

再来看用函数创建一个生成器:

defgen(n):foriinrange(n):yieldi# 创建一个生成器g=gen(5)# <generator object gen at 0x10bb46f50>print(g)# <type 'generator'>print(type(g))# 迭代这个生成器foriing:print(i)# 0 1 2 3 4

在这个例子中,我们在函数中使用 yield 关键字。其实,包含 yield 关键字的函数,不再是一个普通的函数,而返回的是一个生成器。它在功能上与上面的例子一样,可以迭代生成器中的所有数据。

通常情况下,我们习惯在函数内使用 yield 的方式来创建一个生成器。

但是,使用生成器迭代数据相比于普通方式迭代数据,有什么优势呢?

这就要来看一下使用 yield 的函数和使用 return 的普通函数,有什么区别了。

使用 yield 的函数与使用 return 的函数,在执行时的差别在于:

包含 return 的方法会以 return 关键字为最终返回,每次执行都返回相同的结果

包含 yield 的方法一般用于迭代,每次执行时遇到 yield 就返回 yield 后的结果,但内部会保留上次执行的状态,下次继续迭代时,会继续执行 yield 之后的代码,直到再次遇到 yield 后返回

当我们想得到一个集合时,如果使用普通方法,只能一次性创建出这个集合,然后 return 返回:

defgen_data(n):# 创建一个集合return[iforiinrange(n)]

但如果此时这个集合中的数据非常多,我们就需要在内存中一次性申请非常大的内存空间来存储。

如果我们使用 yield 生成器的方式迭代这个集合,就能解决内存占用大的问题:

forgen_data(n):foriinrange(n):# 每次只返回一个元素yieldi

使用生成器创建这个集合,只有在迭代执行到 yield 时,才会返回一个元素,在这个过程中,不会一次性申请非常大的内存空间。当我们面对这种场景时,使用生成器就非常合适了。

其实,生成器在 Python 中还有很大的用处,我会在后面的文章讲解 yield 时,再进行详细的分析。

总结

总结一下,这篇文章我们主要分析了 Python 中「容器」、「迭代器」、「可迭代对象」、「生成器」的联系和区别,用一张图表示它们的关系:


如果一个类实现了 __iter__ 和 __next__ 方法,那么它就是一个迭代器。如果只是实现了 __iter__,并且这个方法返回的是一个迭代器类,那么这个类的实例就只是一个可迭代对象,因为它的迭代细节是交给了另一个类来处理。

像我们经常使用的 list、tuple、set、dict 类型,它们并不是迭代器,只能叫做可迭代对象,它们的迭代细节都是交给了另一个类来处理的。由此我们也得知,一个迭代器一定是一个可迭代对象,但可迭代对象不一定是迭代器。

而生成器可以看做是一个特殊的迭代器,同时它也是一个可迭代对象。使用生成器配合 yield 使用,我们可以实现懒惰计算的功能,同时,我们也可以用非常小的内存,来迭代一个大集合中的数据。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容