python高级特性之生成器与迭代器
一 迭代器
1.1 迭代的概念
了解迭代器,必须先弄明白什么是迭代。
定义:迭代是重复反馈过程的活动,其目的通常是为了逼近所需目标或结果。每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值(以region growing为例)。
那么下面的while循环的例子是否是否是一个迭代的过程呢?
count=0 while count<10: print(“oldboy”) count+=1
答案是否定的。因为这个过程只是满足了迭代的第一个条件:重复,而并没有以每次的结果作为下次的初始值,而这才是迭代的关键。
l=[‘a’,’b’,’c’,’d’,’e’] index=0 while index < len(l): print(l[index]) index+=1
这次while循环才是一个迭代过程,不仅满足重复,而且以每次重新赋值后的index值作为下一次循环中新的索引进行取值,反复迭代,最终可以得到所有的列表中的值。通过索引的方式进行迭代取值,实现简单,但适用序列类型,如字符串,列表,元组。对于没有索引的:字典,集合等非序列类型,以及用户自定义的对象,这种按索引迭代取值的方式便不再适用。
1.2 迭代器对象
针对这个问题,python提供了一种统一的查询方式:迭代器。
在具体讲迭代器之前,先了解一个概念:可迭代对象(Iterable)。之前在数据类型中介绍的容器对象(列表,元组,字典,集合等)都是可迭代对象;从语法形式上讲,能调用__iter__方法的数据对象就是可迭代对象:
>>> [1,2,3].__iter__() <listiterator object at 0x10221b150> >>> {'name':'alvin'}.__iter__() <dictionary-keyiterator object at 0x1022180a8> >>> {7,8,9}.__iter__() <setiterator object at 0x1021ff9b0>
obj.__iter__()方法调用后返回的就是一个迭代器对象(Iterator)。迭代器对象的特性就是能够调用__next__方法依次计算出迭代器中的下一个值。基于此就可以实现无论是否数据为序列对象,都可以通过迭代取值的方式完成查询功能。
>>> s={1,2,3} >>> i=s.__iter__() # 返回可迭代对象s的迭代器对象i >>> i.__next__() # 从第一个元素开始,i通过__next__方法就可以得到可迭代对象s的下一个值。 1 >>> i.__next__() 2 >>> i.__next__() 3 >>> i.__next__() #迭代结束,没有下一个值时调用__next__()抛出StopIteration的异常 Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
解析:针对一个没有索引的可迭代数据类型,我们执行s.__iter__()方法便得到一个迭代器,每执行一次i.__next__()就获取下一个值,待所有值全部取出后,就会抛出异常StopIteration,不过这并不代表错误发生,而是一种迭代完成的标志。需要强调的是:此处我们迭代取值的过程,不再是通过索引而是通过__next__方法。
提示:可以用iter(s)取代s._iter__(),其实iter(s)本质就是在调用s.__iter__(),这与len(s)会调用s.__len__()是一个原理,同理,可以用next(i)取代i.__next__()。obj.__iter__()方法的调用后返回的就是一个迭代器对象
于是我们我们可以将之前基于索引的迭代换成迭代器的形式,如下:
#while的形式 l=['a','b','c','d','e'] i=iter(l) while 1: try: print(next(i)) except StopIteration: break
解析:获取迭代器赋值给i,然后循环内用try监测print语句可能发生的异常,每次执行next(i)获取一个值并打印,直到值完全取尽则抛出异常StopIteration被except捕捉到,执行break跳出循环。
1.3 你不了解的for循环
之前的学习只知道for循环是用来遍历某个数据对象的。但for循环内部到底是怎么工作的,关键字in后面可以放什么数据类型呢?让我们带着这些疑问一起去解析for循环的实现机制。
#for循环的形式: for val in obj: print(val)
解析:关键字in后面数据对象必须是可迭代对象。for 循环首先会调用可迭代对象内的__iter__方法返回一个迭代器,然后再调用这个迭代器的next方法将取到的值赋给val,即关键字for后的变量。循环一次,调用一次next方法,直到捕捉StopIteration异常,结束迭代。解析:关键字in后面数据对象必须是可迭代对象。for 循环首先会调用可迭代对象内的__iter__方法返回一个迭代器,然后再调用这个迭代器的next方法将取到的值赋给val,即关键字for后的变量。循环一次,调用一次next方法,直到捕捉StopIteration异常,结束迭代。
l=[11,22,33] for i in l: #调用iter方法返回一个关于[11,22,33]的迭代器 print(i) #迭代器调用next方法返回的值赋值给i,即i=next(iter(l)) #执行结果 11 22 33
1.4 迭代器的优缺点
对比之前基于索引实现迭代的方式,所有迭代的值都保存到内存中(每次迭代都是基于索引来取值),而迭代器方式则是需要一个值时,通过调用__next__方法临时计算出,然而这种处理数据的方式优点是很明显的:
1、为序列和非序列对象提供了一种统一的迭代方式。
2、惰性计算:python中的Iterator对象只是在需要时才去不断调用next()来计算一个个值,没错,就是计算,是你需要时,临时计算出,之前是不存在的。就迭代器本身来说,同一时刻在内存中只有一个值,因而可以存放无限大的数据流,而对于其他容器类型,如列表,需要把所有的元素都存放于内存中,受内存大小的限制,可以存放的值的个数是有限的。
比如自定义的斐波那契数列迭代器,无论查询第几位数的值,在内存中的开销都是有限的:
class Fab(object): def __init__(self, max): self.max = max self.n, self.a, self.b = 0, 0, 1 def __iter__(self): return self def next(self): if self.n < self.max: r = self.b self.a, self.b = self.b, self.a + self.b #这次结果作为下次的初始值 self.n = self.n + 1 return r raise StopIteration() ''' >>> for key in Fabs(5): print key 1 1 2 3 5 ''' 刚才说过,很多容器都是可迭代对象,此外还有更多的对象同样也是可迭代对象, 比如处于打开状态的文件: f=open("a.txt") # 调用f的iter方法,返回一个关于这个文件的迭代器对象,对其调用next方法得到一行内容赋值给i for i in f: # print(i.strip())
相比于for line in f.readlines()形式将文件所有内容存到内存,迭代器的形式会明显节省内存,效率更高。
迭代器使用是有限制的:
(1)只能一个一个地向后移动且无法预知长度
(2)不能回到开始
(3)无法复制一个迭代器
l=iter([11,22,33]) for i in l: print(i) # 如果有两个或者多个循环使用同一个迭代器,必然只会有一个循环能取到值。 for i in l: # l已经迭代结束,在这里为空,所以这次for循环不会执行。 print("i:",i) #执行结果 11 22 33
迭代器由于对内存存储的优化在py3中已经大量使用。
另外,我们可以通过isinstance()判断一个对象是否是Iterable和Iterator:
>>> from collections import Iterable,Iterator >>> isinstance([1,2,3],Iterable) True >>> i=iter('abc') >>> isinstance(i,Iterator) True
1.5 迭代器协议
迭代器协议要求迭代对象具有__iter__()和__next__()两个方法,__next__之前讲过,是用于计算下一个值的,而__iter__则是返回迭代器本身,目的是使for循环可以遍历迭代器对象,for循环的本质是调用被迭代对象内部的__iter__方法将其变成一个迭代器然后进行迭代取值的操作,如果对象没有__iter__方法则会报错。所以可以说,迭代器对象都是可迭代对象就是因为其内部定义了__iter__方法。
这里自定义一个迭代器对象作为了解:pass
二 生成器
2.1 生成器与yield
简单说,生成器就是使用了yield关键字的函数:
>>> def countdown(n): ... print('countdown start') ... while n > 0: ... yield n ... n-=1 ... print('Done!')
>>> countdown <function countdown at 0x102212f50>
>>> countdown(5) <generator object countdown at 0x1021ff9b0> >>> gen=countdown(3) # 验证对象是否为迭代器对象 >>> gen <generator object countdown at 0x101be0a40> >>> from collections import Iterator >>> isinstance(gen,Iterator) True
countdown是一个函数名,但是调用函数countdown(5)时,并没有像函数一样执行函数体,而是返回了一个生成器对象(generator object)。
生成器本质就是一个迭代器,也可以调用__iter__和__next__方法,只不过这两个方法是由yield关键字在函数调用时封装好的,不用再自己定义,所以,生成器也被称为优雅的迭代器。
>>> gen=countdown(3) >>> gen.__next__() countdown start 3 >>> gen.__next__() 2 >>> gen.__next__() 1 >>> gen.__next__() Done! Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
解析:生成器函数调用时只会返回一个生成器对象。只有当生成器对象调用__next__方法时才会触发函数体代码执行,直到遇到关键字yield停止,将yield后的值作为返回值返回,所以,yield类似于return的功能,但不同于return的是,return返回,函数结束;而yield将函数的状态挂起,等待生成器对象再次调用__next__方法时,函数从挂起的位置后的第一条语句继续运行直到再遇见yield并返回其后的值;如果不断调用__next__方法,最后一次进入函数体,待执行代码不再有yield此时报出迭代异常的错误。
既然生成器对象属于迭代器,那么必然可以使用for循环迭代:
对比迭代器,生成器对象新增一个内置的close方法用来关闭自己,如下:
>>> c=countdown(7) >>> c <generator object countdown at 0x10123f308> >>> next(c) countdown start 7 >>> next(c) 6 >>> c.close() >>> next(c) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
解析:首先获取生成器对象c,然后执行两次next后,调用c.close()关闭自身,这意味着虽然没有迭代到最后一次也不能再通过c取下一个值,所以next(c)抛出迭代结束异常。
yield的功能总结:
(1)封装iter和next方法
(2)执行函数时遇到yield返回其后的值,不同于return,yiled可以返回多次值
(3)挂起函数的状态,等待下一次调用next方法时找到对应的暂停位置继续执行。
2.2 生成器表达式
创建一个生成器对象有两种方式,一是通过在函数中创建yield关键字来实现。另一种就是生成器表达式,这是一种类似于数据类型中学过的列表生成式的语法格式,只是将[]换成(),即:
(expression for item in iterable if condition)
不同于列表生成式最后返回一个列表结果,生成器表达式顾名思义会返回一个生成器对象,比如:
>>> [x*x for x in range(4)] # [0, 1, 4, 9] >>> gen=(x*x for x in range(4)) >>> gen <generator object <genexpr> at 0x101be0ba0>
当需要用到其中的值时,再通过调用next方法或者for循环将值一个个地计算出来:
>>> next(gen) 0 >>> next(gen) 1 >>> next(gen) 4 >>> next(gen) 9 >>> next(gen) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration #------------for 循环------------------ >>> gen=(x*x for x in range(4)) >>> for i in gen: ... print(i) ... 0 1 4 9
优点自然是节省内存,一次只产生一个值在内存中。举一个简单的应用:查找某文件中最长的行的长度。简单实现:
def f(): f = open('FILENAME', 'r') longest = 0 while True: linelen = len(f.readline().strip()) if not linelen: break if linelen > longest: longest = linelen f.close() return longest
存在问题:比如一个日志文件,多个进程都想操作它,所以尽量不要让一个程序占用这个文件句柄太久的时间,改进如下:
def f1(): f = open('FILENAME', 'r') allLinesLen = [line(x.strip()) for x in f] f.close() return max(allLinesLen)
最后一个问题,即对于每一行处理完生成的数据会全部加载到内存,大量占用内存空间,所以可以用生成器表达式替换:
f = open('FILENAME', 'r') longest = max(line(x.strip()) for x in f) f.close() return longest
那是不是写成一句话更简单呢?
return max(line(x.strip()) for x in open('FILENAME'))
虽然更简单,但并不推荐这样写,因为这样每次循环都需要重新执行open()函数,会降低运行效率。