Python基础知识——迭代器与生成器
迭代器与生成器
可迭代对象(iterable)
官方定义:
满足以下条件之一的就是可迭代对象:
- 可以for循环,即for i in iterable
- 可以按index索引, 即实现了__getitem__()方法
- 定义了__iter__方法,返回一个由自身形成的迭代器
- 可以调用iter(obj),并且返回一个迭代器
个人理解:
可迭代对象并不是指某种特定的数据类型,而是一类具有共同特性的的数据结构的总称
这类数据结构都可以重复遍历和索引
其中存储的数据都可以作为一个个单元取出来,不同的可迭代对象可以有不同的方式
与迭代器有着密不可分的联系,可以通过iter()方法生成迭代器
迭代器(iterator)
官方定义:
满足以下条件的称为迭代器:
- 只能迭代一次
- 定义了__iter__()方法,但只能返回自身
- 定义了__next__()方法,返回迭代器中的下一个值,如果没有下一个值则抛出StopIteration错误
- 可以保持当前的迭代状态
可迭代对象与迭代器的比较:
>>> a = [1, 2, 3] >>> b = (1, 2, 3) >>> c = {'1':'a', '2':'b'} #不同可迭代对象的定义,文件对象也是可迭代对象 >>> d = {1, 2, 3} >>> aa = iter(a) >>> bb = iter(b) #通过Iter()方法生成迭代器对象 >>> cc = iter(c) >>> dd = iter(d) >>> aa <list_iterator object at 0x02D14B70> #列表迭代器 >>> bb <tuple_iterator object at 0x002D1C50> #元组迭代器 >>> cc <dict_keyiterator object at 0x02D6C4E0> #字典键迭代器 >>> dd <set_iterator object at 0x02D63918> #集合迭代器 >>> next(aa) 1 >>> next(bb) 1 >>> next(cc) #通过next()方法取出迭代器中的值 '1' >>> next(dd) #可以中断上一个迭代器的取值过程 1 >>> next(cc) #再次回到这个过程,依然接着往下取值,不会重新开始取值 '2' >>> next(cc) #取指结束后则会提示迭代终止,只能进行一次迭代 Traceback (most recent call last): File "<pyshell#34>", line 1, in <module> next(cc) StopIteration >>> a.__iter__() <list_iterator object at 0x02D595F0> #返回一个迭代器对象 >>> aa.__iter__() <list_iterator object at 0x02D14B70> #返回自己 >>> a.__next__() #可迭代对象没有__next__()方法 Traceback (most recent call last): File "<pyshell#39>", line 1, in <module> a.__next__() AttributeError: 'list' object has no attribute '__next__' >>> aa.__next__() 2
如上所示即为迭代器与可迭代对象之间的区别与联系:
可迭代对象有多种多样的类型,而迭代器就是具体的某一类型的迭代器
可迭代对象没有__next()方法,迭代器有,可迭代对象的__iter()方法返回的时一个同类型的迭代器,迭代器的该方法返回的是自身
可迭代对象可进行多次迭代,且不保存迭代状态,即迭代终止则需重新迭代
迭代器只能进行一次迭代,但保存迭代状态,迭代终止了之后再开始依然可以从原状态进入
#可迭代对象与迭代器关系模拟: class Data(object): #可迭代对象, 仅具有初始化和生成迭代器的功能 def __init__(self, *args): self.data = list(args) def __iter__(self): return DataIterator(self) class DataIterator(object): #迭代器,接收可迭代对象的调用,作为生成的迭代器 def __init__(self, data): self.data = data.data self.ind = 0 def __iter__(self): return self def __next__(self): if self.ind == len(self.data): raise StopIteration else: data = self.data[self.ind] self.ind += 1 return data 代码测试: >>> a = Data(1, 2, 3, 4, 5) >>> b = a.__iter__() >>> a <__main__.Data object at 0x02D59E10> #生成一个定义的可迭代对象 >>> b <__main__.DataIterator object at 0x02D59F30> #由这个可迭代对象生成的迭代器 >>> for i in a: #可以生成迭代器的可迭代对象即可进行for迭代 print(i) 1 2 3 4 5 >>> for i in b : #迭代器的迭代 print(i) 1 2 3 4 5 >>> a = Data(1, 2) >>> b = iter(a) #python3默认iter()方法等价于__iter__() >>> b <__main__.DataIterator object at 0x02D59CF0> >>> next(b) #next()方法等价于__next__() 1 >>> b.__next__() 2 >>> next(b) Traceback (most recent call last): File "<pyshell#156>", line 1, in <module> next(b) File "<pyshell#126>", line 9, in __next__ raise StopIteration StopIteration #抛出异常
关于for语句的应用对象:
for语句的运行过程:
实际上一共做了三件事:
- 第一件:通过python自动调用__iter__()方法, 生成迭代器
- 第二件:通过python自动调用__next__()方法,将元素逐个取出并返回
- 第三件:检查迭代器的边界,使其迭代完成后不会越界并且不会抛出StopIteration异常
综上所述, 能执行for语句的条件就是:
首先, 具有__iter__()方法,并且该方法可以生成一个迭代器
其次, 生成的迭代器必须要是一个迭代器,即必须要有__next__()方法
最后, 当next()进入边界时要能返回StopIteration异常
因为这样,一般的迭代器都可以直接调用for语句,因为其__iter__()方法返回的是自身,而且都有__next__()方法,而且带有__iter__()方法的对象也都能调用for语句,正如上例Data对象所示。
又因为Python系统默认iter()方法就是返回一个迭代器,而且迭代器由被定义为必须有__next__()和__iter__()方法,所以可以说,含有__iter__()方法的对象就是可迭代对象, 或者也可以说可以调用for语句的一定是可迭代对象或者迭代器。
#斐波那契迭代器 class Fib(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 #每调用一次next()方法就把下一个数生成出来,并不是最开始存储好的 self.n = self.n + 1 return r raise StopIteration() ############################################################### #self.a, self.b = self.b, self.a + self.b #这种赋值方式属于更简洁更高级的赋值计算方式 #其运算过程如下: >temp1 = self.b >temp2 = self.a + self.b >self.a = temp1 >self.b = temp2 #这种方式可以替换掉通过中间变量的赋值方式,而且不会出错 ############################################################### 代码测试: >>> for i in Fib(5): print(i) 1 1 2 3 5 >>> a = Fib(1) >>> next(a) 1 >>> next(a) Traceback (most recent call last): File "<pyshell#176>", line 1, in <module> next(a) File "<pyshell#170>", line 13, in __next__ raise StopIteration() StopIteration
迭代器只能迭代一次的原因:
如上所述代码所示,在每次迭代时,迭代器并不是如列表、元组等可迭代对象一样,本来讲所有的变量或者值直接都存在内存中,然后直接从内存取,而迭代器每次调用next()方法时,才会将要输出的值运算出来并返回,并没有事先就在内存中存好,所以,迭代器要比可迭代对象更加节约内存,这也是迭代器的优点之一。
当然,这并不是迭代器只能迭代一次的原因,这个原因还是有关于python的垃圾回收机制。
首先说一下python的垃圾回收机制,对于一块内存,如果被赋值给了一个变量,那么就说这块内存被这个变量引用了。
例如:
a = 3
存储3这个数字的这块内存就被变量a引用了
所以,为了不让内存浪费,python规定,当一块内存没有被任何变量引用时,垃圾回收机制就会在一定时间内把这块内存回收,并且这块内存可被其他数据覆盖,即原本存在在这块内存的数据就再也找不到了。
正如前面所说的,迭代器迭代出的每个值都是现生成的,并不是原本就存在的,所以这些值,都没有被变量引用,也就是说,如果这些值不在被生成的时候赋值给变量的话,这些值就直接被垃圾回收机制回收。
所以迭代一次后生成的这些值就都没了,而且迭代器是单向的,下一个值不能生成上一个值,所以如果还想再次迭代,就只能重新生成迭代器。
这就是迭代器只能迭代一次的原因。
这也是迭代器与可迭代对象最大的不同, 迭代器的数据不占内存,而可迭代对象却占据了大量内存,所以在不同情况下,两者的用途就大为不同,尽管迭代器不能回头,但是对于大数据来说,迭代器的性能显然远远超过可迭代对象。
在实际应用中,迭代器的应用要比可迭代对象广得多。
生成器(generator)
生成器是什么:
生成器实质上就是迭代器,但迭代器不一定是生成器,生成器是一类特殊的迭代器,比一般的迭代器要更为优雅,这也是python吸引人的原因之一。
为什么会有生成器:
如前面对迭代器的介绍可知,如果想得到一个迭代器,要么通过iter()方法从可迭代对象转化过去,要么自己构造迭代器对象,显然代码非常复杂。
这两种构造方式让迭代器的使用大大受限,这就是生成器出现的原因。
生成器即具有迭代器优良的性能,而且又具有极其简单的构造方式。
生成器有个特性即为惰性计算,与迭代器相同, 生成器也是调用时才开始计算所调用的元素并返回,并不会事先都计算好。
生成器的构造:
方法一:生成器表达式
>>> a = (x for x in range(40) if x%3==0) #生成器表达式 >>> a <generator object <genexpr> at 0x02D72B10> >>> for i in a: print(i) 0 3 6 9 12 15 18 21 24 27 30 33 36 39 >>> next(a) #next()方法,可以抛出异常 Traceback (most recent call last): File "<pyshell#182>", line 1, in <module> next(a) StopIteration
生成器表达式格式完全同列表生成式,仅仅将使用时外层的中括号改成小括号即可生成生成器,能生成生么样的数据完全由表达式而定,其余功能与迭代器完全一样
方法二:yield方法
>>> def fib(n): #斐波那契函数 before, after = 0, 1 for i in range(n): print(after) before, after = after, before + after >>> fib(5) 1 1 2 3 5 >>> def fib(n): #斐波那契生成器 before, after = 0, 1 for i in range(n): yield after before, after = after, before + after >>> a = fib(5) >>> a <generator object fib at 0x02D72CC0> >>> next(a) 1 >>> next(a) 1 >>> next(a) 2 >>> next(a) 3 >>> next(a) 5 >>> next(a) Traceback (most recent call last): File "<pyshell#205>", line 1, in <module> next(a) StopIteration
由上述代码中,斐波那契函数和斐波那契生成器比较很容易可以看出,只有一行不同,就形成了一个生成器,这就是生成器的强大之处,创建方式几乎等同于函数,但具有迭代器的全部功能,包含yield的函数就叫做生成器函数,其返回值就是一个生成器。
关于yield:
#示例一:yield功能相当于return >>> def test(): print("ok") yield print("Not good") return >>> a = test() >>> a <generator object test at 0x02D72C90> >>> next(a) ok >>> next(a) Not good Traceback (most recent call last): File "<pyshell#215>", line 1, in <module> next(a) StopIteration #示例二:yield可以记住上次运行的状态 >>> def test(): print("ok1") yield 1 print("ok2") yield 2 print("not ok") >>> a = test() >>> b = next(a) ok1 >>> b 1 >>> c = next(a) ok2 >>> c 2 >>> d = next(a) not ok Traceback (most recent call last): File "<pyshell#228>", line 1, in <module> d = next(a) StopIteration #示例三:调用for迭代生成器 >>> def test(): print('ok1') yield 1 print('ok2') yield 2 print('ok3') yield 3 print('may i ok?') return 4 >>> a = test() >>> for i in a: print(i) ok1 1 ok2 2 ok3 3 may i ok?
yield关键字就是一个python封装的语法糖,内部已经实现了迭代器协议,所以可以直接形成生成器对象 。
yield用法和特性都如上面的例子所表现的:
- yield相当于函数的return,当函数运行到这时,返回返回值,并记录当前状态,yield后面的值作为返回值
- 当函数再次运行时,从上次记录的位置开始运行
- 生成器函数遇到return时会直接抛出StopIteration的错误,而且不会再迭代下去
- 当用for对生成器进行迭代时,之所以不会报出异常,是因为for内部已经做了处理,直接忽视异常
示例代码分析:
def add(x, y): return x+y def gen(): for i in range(4): yield i base = gen() for n in [1, 10]: base = (add(i, n) for i in base) print(list(base))
求这段代码输出的结果
结果:[20, 21, 22, 23]
分析:
这个程序的难点就在于生成器的惰性计算,生成器只有在真正使用的时候才会通过运算获得元素。
所以对于这个程序来说,首先base等于可以生成0, 1, 2, 3的生成器。
然后进入for循环,循环进行了两次,只是要注意,这两次仅仅是赋值,并没有让base生成元素,而base循环之后记录的值,就是第二次的10, 11, 12, 13。
然后,到最后list方法时,要取出生成器中的元素,于是就开始计算,然后就找到生成器生成元素的过程,
就是(add(i, n) for i in base)
所以,此时的base是可以生成10, 11, 12, 13的生成器,n=10
所以,最终输出的结果就是[20, 21, 22, 23]
生成器的方法:
方法一:send()
>>> def test(): ret = yield print(ret) yield 'ok', [1,2] >>> a = test() >>> next(a) >>> a.send('Hello world') Hello world ('ok', [1, 2])
send()方法如代码所示,可以往挂起状态的生成器函数中传递参数,前提是有一个挂起的yield语句,然后send()方法的参数就可以赋值给函数中的变量,如果没有挂起的yield调用send方法会报错
所以,一般来说,执行send之前,都会执行一次next()语句
>>> a = test() >>> a.send(None) >>> a.send('ni hao') ni hao ('ok', [1, 2])
如果没有挂起的yield,执行send(None)等价于next()语句,但是不推荐这样使用
方法二:throw()
>>> def test(): n = 1 while True: yield n n += 1 >>> a = test() >>> next(a) 1 >>> next(a) 2 >>> next(a) 3 >>> a.throw(Exception, "Method throw called") Traceback (most recent call last): File "<pyshell#360>", line 1, in <module> a.throw(Exception, "Method throw called") File "<pyshell#355>", line 4, in test yield n Exception: Method throw called >>> next(a) Traceback (most recent call last): File "<pyshell#361>", line 1, in <module> next(a) StopIteration >>> a = test() >>> next(a) 1 >>> a.throw(NameError, 'I do not know you!') Traceback (most recent call last): File "<pyshell#364>", line 1, in <module> a.throw(NameError, 'I do not know you!') File "<pyshell#355>", line 4, in test yield n NameError: I do not know you!
如上所示,throw方法可以在生成器执行时,抛出一个自定义的异常,并且使得生成过程结束
>>> def test(): n = 1 while True: try: yield n n += 1 except: n = 0 >>> a = test() >>> next(a) 1 >>> a.throw(Exception, "Nothing") 0 >>> next(a) 1
如果在生成器中加入一个try-except语句块处理异常,则会执行异常处理语句,之后继续执行生成器操作,此时就不会结束生成器
方法三:close()
>>> def test(): try: yield 1 print("statement after yield") except GeneratorExit: print("Generator error caught") print("end of Generator") >>> a = test() >>> next(a) 1 >>> next(a) statement after yield end of Generator Traceback (most recent call last): File "<pyshell#393>", line 1, in <module> next(a) StopIteration >>> a = test() >>> a.close() >>> next(a) Traceback (most recent call last): File "<pyshell#396>", line 1, in <module> next(a) StopIteration >>> a = test() >>> next(a) 1 >>> a.close() Generator error caught end of Generator
close()方法的功能如上所示:
- 如果没有yield挂起时,相当于next()
- 如果有yield挂起时,会引发一个GeneratorExit异常,并结束程序
- 通过try-except语句捕捉到该异常之后可以添加一些结束程序时的处理语句
- 如果没有捕捉,则直接结束生成器
另外:
关于GeneratorExit异常:
生成器结束时都会抛出该异常
只有生成器运行过next()方法后才可能会出现该异常
如果不捕捉这个异常生成器就会直接结束
如果捕捉则可以添加一些异常处理程序

浙公网安备 33010602011771号