Python中的迭代器与生成器
在Python中存在两种好用的功能:迭代器与生成器。以list容器为例,在使用该容器迭代一组数据时,必须事先将所有数据存储到容器中,才能开始迭代;而生成器却不同,它可以实现在迭代的同时生成元素。
也就是说,对于可以用某种算法推算得到的多个数据,生成器并不会一次性生成它们,而是什么时候需要,才什么时候生成。
迭代器
迭代器是一个可以记住遍历的位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。
Python中迭代器协议主要用到了两个魔法方法:__iter__(),__next__()
__iter__()方法创建一个具有__next__()方法的迭代器对象
__next__()方法返回下一个迭代器对象
只具有迭代操作的生成器,也是属于迭代器的。
字符串,列表或元组对象都可用于创建迭代器:
>>> list=[1,2,3,4]
>>> it = iter(list) # 创建迭代器对象
>>> print (next(it)) # 输出迭代器的下一个元素
1
>>> print (next(it))
2
迭代器对象可以使用常规for语句进行遍历:
>>> list=[1, 2, 3, 4]
>>> it = iter(list) # 创建迭代器对象
>>> for x in it:
>>> print (x, end=" ")
1 2 3 4
创建迭代器
把一个类作为一个迭代器使用需要在类中实现两个方法 __iter__() 与 __next__()。
__iter__() 方法返回一个特殊的迭代器对象, 这个迭代器对象实现了__next__()方法并通过 StopIteration异常标识迭代的完成。
__next__() 方法会返回下一个迭代器对象。
创建一个返回数字的迭代器,初始值为 1,逐步递增 1:
class MyNumbers:def __iter__(self):self.a = 1return selfdef __next__(self):x = self.aself.a += 1return xmyclass = MyNumbers()
myiter = iter(myclass)
for x in myiter:print(x)
StopIteration
StopIteration异常用于标识迭代的完成,防止出现无限循环的情况,在__next__()方法中我们可以设置在完成指定循环次数后触发StopIteration异常来结束迭代。
在 20 次迭代后停止执行:
class MyNumbers:def __iter__(self):self.a = 1return selfdef __next__(self):if self.a <= 20:x = self.aself.a += 1return xelse:raise StopIterationmyclass = MyNumbers()
myiter = iter(myclass)
for x in myiter:print(x)
生成器
生成器(generator)从字面意思上理解,就是循环计算的操作方式。在Python中,提供一种可以边循环边计算的机制。
生成器是解决使用序列存储大量数据时,内存消耗大的问题,而且可以避免不必要的计算,带来性能上的提升。我们可以根据存储数据的某些规律,演算为算法,在循环过程中通过计算得到,这样可以不用创建完整序列,从而大大节省占用空间。
跟普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作,更简单点理解生成器就是一个迭代器。
在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值,并在下一次执行 next() 方法时从当前位置继续运行。
调用一个生成器函数,返回的是一个迭代器对象。
实现生成器的两种方式
第一种方法:把一个列表生成式的[]改成(),就创建了一个生成器。
>>> L=[x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x00000236B9435C10>
这里一定要注意把[]改成()后,不是生成一个tuple,而是生成一个generator。
第二种方法:在函数中使用yield关键字,函数就变成了一个生成器。调用该函数,就等于创建了一个生成器对象。
函数里有了yield后,执行到yield就会停住,当需要再往下算时才会再往下算。所以生成器函数即使是有无限循环也没关系,它需要算到多少就会算多少,不需要就不往下算。
一个生成器,主要是通过循环反复调用next()方法,直到捕获异常。
来一个例子说明一下生成器的用法:
def intNum():print("开始执行")for i in range(5):yield iprint("继续执行")
num = intNum()
和普通函数不同,intNum() 函数的返回值用的是yield关键字,而不是return关键字,此类函数就称为生成器函数。调用生成器函数,就可以创建一个 num 生成器对象。
生成器的使用有三种方法:
#调用 next() 内置函数
print(next(num))#调用 __next__() 方法
print(num.__next__())#通过for循环遍历生成器
for i in num: print(i)
程序执行结果:
开始执行
0
继续执行
1
继续执行
2
继续执行
3
继续执行
4
继续执行
程序的执行流程:
首先,在创建有num生成器的前提下,通过其调用next()内置函数,会使 Python 解释器开始执行 intNum() 生成器函数中的代码,因此会输出“开始执行”,程序会一直执行到yield i,而此时的 i=0,因此 Python 解释器输出“0”。由于受到 yield 的影响,程序会在此处暂停。
然后,我们使用 num 生成器调用 __next__() 方法,该方法的作用和 next() 函数完全相同(事实上,next() 函数的底层执行的也是 __next__() 方法),它会是程序继续执行,即输出“继续执行”,程序又会执行到yield i,此时 i=1,因此输出“1”,然后程序暂停。
最后,我们使用for循环遍历num生成器,之所以能这么做,是因为for循环底层会不断地调用next()函数,使暂停的程序继续执行,因此会输出后续的结果。
如果此时再调用next()函数,此时程序会报错,因为生成器执行完毕后辉捕捉异常。
Traceback (most recent call last):File "D:\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py", line 3418, in run_codeexec(code_obj, self.user_global_ns, self.user_ns)File "<ipython-input-40-c3aa4ea10659>", line 1, in <module>next(num)
StopIteration
因此,带yield的函数具体内部执行操作为:
yield方法:相当于return作用,程序遇到yield则直接中止后续步骤;
当再次调用生成器时,next()方法会唤醒,并继续执行yield后续步骤;
还可以调用send()方法,可以唤醒,并传入一个值,继续执行yield后续步骤。
生成器是可迭代的,每一次只可读一次。因此常常与for循环一起组合使用。
生成器案例
案例1:“生成器是解决使用序列存储大量数据时,内存消耗大的问题”的代码证明:
import sysdef test(n): print("start") while n > 0: yield n n-=1 print("end")a = [x for x in range(1000)]
b= test(1000)
print("a内存大小:",sys.getsizeof(a))
print("b内存大小:",sys.getsizeof(b))
程序执行结果:
a内存大小: 9016
b内存大小: 112
案例2:使用函数生成器打印斐波那契数列。
def fib(length):a, b = 0, 1n = 0while n < length:yield ba, b = b, a + bn += 1return '没有更多的元素了'
程序执行结果:
>>> g = fib(8)
>>> for i in g: print(i)
1
1
2
3
5
8
13
21
案例3:send()方法
def gen():i = 0while i < 5:temp = yield iprint('temp:', temp)i += 1return '没有更多的元素'>>> g = gen()
>>> print(g.send(None))
>>> n1 = g.send('起点')
>>> print('n1', n1)
>>> n2 = g.send('发展')
>>> print('n2', n2)
0
temp: 起点
n1 1
temp: 发展
n2 2
案例4:应用多任务
先设置两个虚拟的任务函数
def task1(n):for i in range(n):print('正在搬第{}块砖!'.format(i))def task2(n):for i in range(n):print('正在听第{}首歌!'.format(i))
现在分别执行这两个任务:
>>> task1(10)
>>> task2(5)正在搬第0块砖!
正在搬第1块砖!
正在搬第2块砖!
正在搬第3块砖!
正在搬第4块砖!
正在搬第5块砖!
正在搬第6块砖!
正在搬第7块砖!
正在搬第8块砖!
正在搬第9块砖!
正在听第0首歌!
正在听第1首歌!
正在听第2首歌!
正在听第3首歌!
正在听第4首歌!
可以看到,任务并不是交替执行的(非多任务),而是先执行完一个任务,再执行下一个任务。现在用生成器来变成多任务执行。
>>> g1 = task1(10)
>>> g2 = task2(5)
>>> while True:
>>> g1.__next__()
>>> g2.__next__()正在搬第0块砖!
正在听第0首歌!
正在搬第1块砖!
正在听第1首歌!
正在搬第2块砖!
正在听第2首歌!
正在搬第3块砖!
正在听第3首歌!
正在搬第4块砖!
正在听第4首歌!
正在搬第5块砖!
Traceback (most recent call last):File "task.py", line 16, in <module>g2.__next__()
StopIteration
报错用异常捕捉处理一下:
>>> g1 = task1(10)
>>> g2 = task2(5)
>>> while True:
>>> try:
>>> g1.__next__()
>>> g2.__next__()
>>> except:
>>> pass正在搬第0块砖!
正在听第0首歌!
正在搬第1块砖!
正在听第1首歌!
正在搬第2块砖!
正在听第2首歌!
正在搬第3块砖!
正在听第3首歌!
正在搬第4块砖!
正在听第4首歌!
正在搬第5块砖!
正在搬第6块砖!
正在搬第7块砖!
正在搬第8块砖!
正在搬第9块砖!
可迭代对象 VS 迭代器
可迭代对象(Iterable)是可以直接作用于for循环的对象。包括集合数据类型(list、tuple、dict、set、str等)和生成器(generator)。可以使用isinstance()判断一个对象是否是Iterable对象。
>>>from collections import Iterable
>>> isinstance([], Iterable)
True
>>> isinstance({}, Iterable)
True
>>> isinstance('abc', Iterable)
True
>>> isinstance((x for x in range(10)), Iterable)
True
>>> isinstance(100, Iterable)
False
迭代器(Iterator)表示的是一个数据流。Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。
生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。把list、dict、str等Iterable变成Iterator可以使用iter()函数:
>>> isinstance(iter([]),Iterator)
True
>>> isinstance( iter('abc'),Iterator)
True
Python的for循环本质上就是通过不断调用next()函数实现的,例如:
for x in [1, 2, 3, 4, 5]:pass
实际上完全等价于:
it= iter([1,2,3,4,5]) # 获得Iterator对象
while True:try:x = next(it) # 获得下一个值except StopIteration:break # 遇到StopIteration就退出循环
itertools模块
python的内置模块itertools提供了用于操作迭代对象的函数,非常方便实用。举一个例子:
islice(iterable, [start], stop, [step])
创建一个迭代器,生成项的方式类似于切片返回值:iterable[start:stop:step],将跳过前start个项,迭代在stop所指定的位置停止,step指定用于跳过项的步幅。与切片不同,负值不会用于任何**start**,**stop**和**step**,如果省略了start,迭代将从0开始,如果省略了step,步幅将采用1。
举个例子:
from itertools import islicedef fib():a, b = 0, 1while True:yield aa, b = b, a + b>>> f = fib()
>>> print(list(islice(f, 10)))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]