确定不点进来看看?Python面试必问之迭代器、可迭代对象、生成器 | 【知了干货分享】
所有Python程序员都知道,列表可以迭代。其实不止是列表,Python中的序列都可以迭代。所谓序列,指的是其中的元素按一定顺序排列,可通过每个元素所在位置的编号(称为索引)访问它们,序列都可以进行的操作包括索引,切片,加,乘,检查成员。在Python中,序列包括str、list、tuple、bytes、bytearray、collections.deque等。那么序列为什么能够迭代呢,跟着小编一起往下看吧。
壹-可迭代对象
Python解释器需要迭代对象时,例如我们用一个for循环去获取一个列表里的所有元素,这个时候它会自动调用iter()。这个函数在背后都做了什么呢?首先,检查对象是否实现了__iter__()方法,如果实现了就直接调用它返回一个迭代器;如果没有实现__iter__()方法也不要紧,解释器会去找__getitem__()方法,找到之后创建一个迭代器,然后从索引0开始按顺序获取元素;如果找不到__getitem__()方法就会抛出TypeError异常,提示“XXX' object is not iterable”。到这里我们可以得到上一个问题的答案了:Python序列可以迭代的原因是,它们都实现了__getitem__()方法,其实标准的序列也都实现了__iter__()方法。
接下来我们可以给可迭代对象下一个定义了:可迭代对象就是使用内置iter函数能够获取迭代器的对象。并且我们理清了可迭代对象和迭代器之间的关系:Python从可迭代对象获取迭代器。
贰-迭代器
在Python中我们可以通过继承collections.abc.Iterator这个抽象基类来实现自己的迭代器,接下里我们查看它的源代码,发现它定义了两个抽象方法:__iter__()、__next__()。
所以标准的迭代器必须实现__iter__()、next()这两个方法。下面看一个例子:
有了上面的例子,就可以总结一下在可迭代对象与迭代器中的for循环工作机制了。
对于迭代器:
1、调用__iter__(),返回自身。
2、不断调用__next__()方法,产生下一个元素。
3、迭代到最后,抛出StopIteration异常。
对于可迭代对象:
1、先判断对象是否实现了__iter__()或__getitem__()方法,没有实现就抛出TypeError异常,否则调用iter()方法,返回一个迭代器。
2、Python内部不断地调用迭代器的__next__方法,每次按序返回迭代器中的一个值。
3、迭代到最后,抛出StopItera异常。
注意,StopIteration异常表明迭代器要结束了。Python语言内部会自动处理 for循环和其他迭代上下文(如列表推导、元组拆包,等等)中的StopIteration异常。我们也可以如下方式来模拟for循环的工作机制。
用图片来表示for循环背后一系列的过程,小伙伴们能看得更明白:
叁-生成器
提到生成器大家可能都想到了yield关键字。毫无疑问,定义体中有yield关键字的函数都是生成器函数,调用生成器函数会返回一个生成器对象。下面一起来看看生成器的使用:
我们从图中能够看到,生成器函数会创建一个生成器对象,包装生成器函数的定义体。把生成器传给next(…)函数时,生成器函数会向前,执行函数定义体中的下一个 yield 语句,返回产出的值,并在函数定义体的当前位置暂停。每调用一次next()函数就产出一个值。最终,函数的定义体返回时,外层的生成器对象会抛出 StopIteration 异常——这一点与迭代器协议一致。所以生成器都是迭代器。
除了简单的生成器函数,Python还提供了一种更简洁的写法:生成器表达式。它也能得到一个生成器对象。示例如下:
其实在开发环境中,我们选择使用生成器或者迭代器大多是为了省内存,因为不用像列表那样一次性把所有内容全部加载到内存,而是一个值一个值得产出,大大减少了内存的消耗。这样能提高效率的操作何乐而不为呢?所以从Python2到Python3的过渡,做了很多类似的优化。例如:range()函数返回一个类似生成器的对象,调用zip()函数也是得到一个类似生成器的函数等等。
肆-生成器和迭代器对比
在介绍完生成器跟迭代器之后,我们可以来对比一波了:
1、从接口方面
Python的迭代器协议定义了__next__()和__iter__()两个方法。生成器对象实现了这两个方法,因此从这方面来看,所有生成器都是迭代器。
2、从实现方式上
Python提供两种方式实现生成器:含有yield关键字的函数或生成器表达式。调用生成器函数或者执行生成器表达式得到的生成器对象属于语言内部的 GeneratorType(https://docs.python.org/3/library/types.html#types.GeneratorType)类型。从这方面来看,所有生成器都是迭代器,因为GeneratorType 类型的实例实现了迭代器接口。不过,我们可以编写不是生成器的迭代器,方法是实现经典的迭代器模式,就像下面这样:
3、从概念方面
根据《设计模式:可复用面向对象软件的基础》书中的定义,在典型的迭代器设计模式中,迭代器用于遍历集合,从中产出元素。迭代器可能相当复杂,例如,遍历树状数据结构。但是,不管典型的迭代器中有多少逻辑,都是从现有的数据源中读取值;而且,调用 next(it) 时,迭代器不能修改从数据源中读取的值,只能原封不动地产出值。而生成器可能无需遍历集合就能生成值,例如 range 函数。再说得生活化一点,迭代器首先需要一个容器,然后才能从里面一个接一个地把东西拿出来;而生成器更像是凭空从中源源不断地获取我们想要的东西。从概念上,实现方式无关紧要,那么抛开Python的生成器对象,我们也能用自己的方式来编写生成器,例如:
事实上,Python 程序员不会严格区分二者,即便在官方文档中也把生成器称作迭代器。所以小伙伴也不用太纠结两者的区别。还是那句话,生成器一定是迭代器,迭代器不一定是生成器。
伍-小结和补充
说了这么多,可以用一张图片来总结本文目前的内容:
除了上述讲到的内容,还要给大家讲讲iter()函数一个鲜为人知的用法:iter(source, sentinel=None) 。
第一个参数必须是可调用的对象,用于不断调用(没有参数),产出各个值;第二个值是哨符,这是个标记值,当可调用的对象返回这个值时,触发迭代器抛出 StopIteration 异常,而不产出哨符。例如:
更多干货内容,欢迎关注公众号:知了python