迭代器和生成器
-
迭代器:
-
楔子
-
python中的for循环
-
迭代的概念
-
可迭代协议和迭代器协议
-
再谈for循环
-
生成器:
-
初始生成器
-
生成器函数
一、迭代器---楔子
我们都知道在对不同的数据类型的变量进行取值时可以有很多办法,比如str类型数据可以通过索引、切片或for循环的方式将所需子串取出;通过列表的索引,切片、方法或者for循环之类的都可以将列表中的元素取出;而元组呢也可以通过索引、切片、for循环的方式取出,字典可以通过key取出对应的value或者使用for循环取出key或value;而集合只能通过for循环取出里面的元素;
通过上面各数据类型的不同取值方法,我们可以发现他们都有一个共同的取出元素的方法,那就是for循环,而在上面我并没有列出int的取值方法,为什么,因为int不能用for循环来取值,我们来看个例子,通过for循环对一个int数据类型进行循环时会发生什么情况:
>>> num = 123456 >>> for n in num: ... print(n) ... Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'int' object is not iterable #报错了,提示是“类型错误:int对象不可迭代”
不可迭代?这是什么鬼,什么是迭代呢???并且为什么其他的数据类型就可以执行并且不报错呢?for循环到底是怎样的一个机制呢?for循环是通过数据的索引取值?还是通过切片取值?还是根据什么方法来取值的呢?带着这一系列问题,我们继续往下看!
二、python中的for循环
在解释迭代含义之前,我们先来谈一谈python中的for循环;要了解for循环我们还得从代码的角度出发;我们再看一些例子:
#1.首先我们对一个列表进行for循环 >>> li = [1,2,3,4,5,6] >>> for n in li: ... print(n,end=" ") ... 1 2 3 4 5 6 #python成功的帮我们把列表中的元素不多不少的取出来了; #2.我们再对一个数字进行for循环 >>> num = 123456 >>> for n in num: ... print(n) ... Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'int' object is not iterable #这时,程序报错了,错误信息是int不是一个可迭代对象 #3.通过数字循环报错,我们可以逆向认为,for循环的对象可能得是一个可迭代的对象,那么想整明白for循环,看来我们要先了解一下迭代这个词了;
三、迭代的概念
定义:迭代是重复反馈过程的活动,其目的通常是为了逼近所需目标或结果。每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值;
也就是说迭代是一个重复的过程,每次重复即一次迭代,并且每次迭代的结果都是下一次迭代的初始值;如果只是单纯地重复,因而不是迭代;比如:
#单纯的重复 while True: print('===>') #迭代 l=[1,2,3] count=0 while count < len(l): print(l[count]) count+=1
四、可迭代协议和迭代器协议
协议说明:假如我们自己写了一个数据类型,希望这个数据类型里的东西也可以使用for被一个一个的取出来,那我们就必须满足for的要求。这个要求就叫做“协议”。
1.什么是可迭代协议?
定义:可以被迭代要满足的要求就叫做可迭代协议。可迭代协议的定义非常简单,就是内部实现了__iter__方法。
2.什么是可迭代对象?
可迭代对象指的是内置有__iter__方法的对象,即obj.__iter__,也就是说只要这个对象的方法中有__iter__方法,那么我们就叫这个对象为可迭代对象;
3.如何查看某个类的某个对象的方法中有没有这个__iter__方法呢?
我们使用一个函数来检查,dir()函数,这是一个python内置的函数,用来打印对象下的所有方法,那好我们就根据上面所说,去查一查列表、元组、字符串、集合、数字等等看看他们的方法下有没有__iter__这个方法;
#我们将各数据类型下的方法进行取交集,这样的话如果有__iter__的话说明他们都是有这个可迭代方法的;我们先对str、list、tuple、set、dict这几个数据类型进行取交集; st = set(dir("str")) num = set(dir(123)) boo = set(dir(True)) li = set(dir([4,5,"A"])) tu = set(dir(("q","w",))) dic = set(dir({"K1":"V1"})) se = set(dir({1,6,3,9,4})) print(st&li&tu&dic&se) ------------------- 输出结果 ----------------------------------- {'__subclasshook__', '__len__', '__gt__', '__dir__', '__sizeof__', '__doc__', '__format__', '__delattr__', '__setattr__', '__hash__', '__init_subclass__', '__iter__', '__class__', '__new__', '__str__', '__lt__', '__repr__', '__ge__', '__init__', '__le__', '__reduce__', '__ne__', '__getattribute__', '__reduce_ex__', '__contains__', '__eq__'} #可以看出我们对这几个数据类型取交集后得到的结果是有_iter_这个方法的,说明这几个数据类型都是有这个方法的,如果有一个没有,那么这个方法是取不到的; #接下来我们单独来看看int和bool数据类型下的方法有没有_iter_; {'__gt__', '__reduce_ex__', 'denominator', '__ceil__', '__index__', 'numerator', '__floordiv__', '__rshift__', '__rxor__', '__add__', '__new__', '__radd__', '__le__', '__mul__', '__hash__', '__abs__', '__setattr__', '__doc__', '__rpow__', '__rtruediv__', '__and__', '__dir__', 'imag', 'bit_length', '__round__', '__ne__', '__repr__', 'conjugate', '__pow__', '__rdivmod__', 'to_bytes', '__sizeof__', '__rand__', '__divmod__', '__eq__', '__sub__', '__float__', '__rmul__', '__truediv__', '__trunc__', '__init__', '__rfloordiv__', '__neg__', '__lt__', '__delattr__', '__mod__', '__or__', '__rmod__', '__bool__', '__class__', '__rsub__', '__str__', '__xor__', 'real', '__getattribute__', '__init_subclass__', '__rlshift__', '__floor__', 'from_bytes', '__ge__', '__lshift__', '__format__', '__getnewargs__', '__int__', '__ror__', '__subclasshook__', '__rrshift__', '__pos__', '__invert__', '__reduce__'} {'__gt__', '__reduce_ex__', 'denominator', '__ceil__', '__index__', 'numerator', '__floordiv__', '__rshift__', '__rxor__', '__add__', '__new__', '__radd__', '__le__', '__mul__', '__hash__', '__abs__', '__setattr__', '__doc__', '__rpow__', '__rtruediv__', '__and__', '__dir__', 'imag', 'bit_length', '__round__', '__ne__', '__repr__', 'conjugate', '__pow__', '__rdivmod__', 'to_bytes', '__sizeof__', '__rand__', '__divmod__', '__eq__', '__sub__', '__float__', '__rmul__', '__truediv__', '__trunc__', '__init__', '__rfloordiv__', '__neg__', '__lt__', '__delattr__', '__mod__', '__or__', '__rmod__', '__bool__', '__class__', '__rsub__', '__str__', '__xor__', 'real', '__getattribute__', '__init_subclass__', '__rlshift__', '__floor__', 'from_bytes', '__ge__', '__lshift__', '__format__', '__getnewargs__', '__int__', '__ror__', '__subclasshook__', '__rrshift__', '__pos__', '__invert__', '__reduce__'} #我们会发现int和bool里面并没有_iter_方法
因为for循环的对象必须是可迭代的,就是说这个对象必须有_iter_这个方法,for循环才会遍历,所以int类型和bool类型不能用在for循环中;
4.那么这个_iter_方法到底做了什么事情呢?为什么有了这个方法才可以进行for循环?
我们继续通过代码去了解_iter_方法:
#我们找一个可迭代对象让其调用_iter_方法,然后再打印一下: >>> li = [1,2] >>> print(li.__iter__()) <list_iterator object at 0x7fc167b157f0>
执行了print(li.__iter__()) 后,我们得到了一个list_iterator,不认识?没关系,我们找度娘问问;
什么情况?一个可迭代对象执行了_iter_方法后竟然生成了一个迭代器,那么,啥是迭代器啊?
5.什么是迭代器:
定义:迭代器协议是指:对象必须提供一个next方法,执行该方法要么返回迭代中的下一项,要么就引起一个StopIteration异常,以终止迭代 (只能往后走不能往前退);
6.什么是迭代器对象:
迭代器对象指的是内置有__iter__方法和__next__方法的对象,也就是说只要这个对象的方法中有__iter__和__next__方法,那么我们就叫这个对象为迭代器对象;
同样我们使用dir()内置函数来查看,首先我们必须要通过可迭代对象生成一个迭代器对象,然后通过dir()这个迭代器对象来查看它下面的方法:
#我们已经知道list是一个可迭代对象,因为它方法中有_iter_,所以我们用一个列表通过自身的_iter_方法生成一个迭代器对象; >>> li = [1,2,3] #可迭代对象li >>> l1 = li.__iter__() #通过可迭代对象调用iter方法生成的迭代器对象l1 >>> print(dir(l1)) #查看这个迭代器对象下的方法; ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__length_hint__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__']
通过上面的实例,我们可以知道,迭代器对象一定具有__iter__方法和__next__方法,也就是说只要具有这两个方法,我们就可以称这个对象为迭代器对象;
7.迭代器的特点:
#优点: #1.迭代器提供了一种不依赖于索引的取值方式,这样就可以遍历那些没有索引的可迭代对象了(字典,集合,文件) #2.迭代器与列表比较,迭代器是惰性计算的,更节省内存,惰性计算解释:惰性计算就是每次就取一个值,需要我们多次去使用它; #缺点: #1.一次性的,只能往后取值,不能倒着取值; #2.无法获取迭代器的长度,使用不如列表索引取值灵活
8.迭代器的用途,多用在for循环中;
五、再谈for循环
那么,我们理解了以上问题后,再来看看for循环是如何配合迭代器来获取数据的;
for循环的内部机制:
#1.首先for循环的对象是一个可迭代对象, #2.通过可迭代对象下的_iter_方法获取到迭代器, #3.得到迭代器后通过迭代器的_next_方法来获取到数据 #4.将获取到的数据赋值给定义的变量; #5.当遇到迭代器取值完毕后的报错后,for循环通过try异常处理来解决,当遇到报错也就是迭代器取值完毕后,就break退出;
我们可以通过while循环来模拟for循环的操作流程;
>>> li = [1,2,3,4,5] #建立一个具有可迭代的对象; >>> l1 = li.__iter__() #将此对象转变为迭代器对象; >>> while 1: ... try: ... print(l1.__next__()) #迭代器对象使用next方法取值 ... except StopIteration: ... break #取值完毕后遇到异常退出 ... 1 2 3 4 5 ##这样的形式是不是很像for循环那样,其实就是for循环内部实现的机制,不同的是for循环将每次去取到的值进行了变量赋值了;
六、初识生成器
我们知道的迭代器有两种:1.直接调用next方法返回的,2.通过可迭代对象执行iter方法得到;
迭代器好处就是可以节省内存;
如果在某些情况下,也需要节省内存,就只能自己写。我们自己写的这个能实现迭代器功能的东西就叫生成器;
生成器创建:
1.生成器函数
常规函数定义,但是,使用yield语句而不是return语句返回结果。yield语句一次返回一个结果,在每个结果中间,挂起函数的状态,以便下次重它离开的地方继续执行
2.生成器表达式
类似于列表推导,但是,生成器返回按需产生结果的一个对象,而不是一次构建一个结果列表;
生成器Generator:
本质:生成器的本质就是迭代器,所以生成器本省自带了iter和next
特点:和迭代器一样,惰性运算,节省内存,只能向前,开发者可以自定义;
生成器函数:
定义:一个包含yield关键字的函数就是一个生成器函数;
yield说明:yield可以为我们从函数中返回值,但是yield又不同于return,return的执行意味着程序的结束,调用生成器函数不会得到返回的具体的值,而是得到一个可迭代的对象。每一次获取这个可迭代对象的值,就能推动函数的执行,获取新的返回值。直到函数执行结束。
import time def genrator_fun1(): a = 1 print('现在定义了a变量') yield a b = 2 print('现在又定义了b变量') yield b g1 = genrator_fun1() print('g1 : ',g1) #打印g1可以发现g1就是一个生成器 print('-'*20) #我是华丽的分割线 print(next(g1)) time.sleep(1) #sleep一秒看清执行过程 print(next(g1)) ---------------------- 输出结果 ------------------------ g1 : <generator object genrator_fun1 at 0x00000000021EF728> -------------------- 现在定义了a变量 1 现在又定义了b变量 2
生成器的好处。
就是不会一下子在内存中生成太多数据,就是节省内存;
比如:我想让工厂给学生做校服,生产2000000件衣服,我和工厂一说,工厂应该是先答应下来,然后再去生产,我可以一件一件的要,也可以根据学生一批一批的找工厂拿。
而不能是一说要生产2000000件衣服,工厂就先去做生产2000000件衣服,等回来做好了,学生都毕业了。。。
#初识生成器二 def produce(): """生产衣服""" for i in range(2000000): yield "生产了第%s件衣服"%i product_g = produce() print(product_g.__next__()) #要一件衣服 print(product_g.__next__()) #再要一件衣服 print(product_g.__next__()) #再要一件衣服 num = 0 for i in product_g: #要一批衣服,比如5件 print(i) num +=1 if num == 5: break #到这里我们找工厂拿了8件衣服,我一共让我的生产函数(也就是produce生成器函数)生产2000000件衣服。 #剩下的还有很多衣服,我们可以一直拿,也可以放着等想拿的时候再拿 初识生成器二
使用生成器写一个监听文件输入例子:
import time def tail(filename): f = open(filename) f.seek(0, 2) #从文件末尾算起 while True: line = f.readline() # 读取文件中新的文本行 if not line: time.sleep(0.1) continue yield line tail_g = tail('tmp') for line in tail_g: print(line)
send关键字
含义:和next方法一样也是取下一个值,不同的是,send还可以给上一个yield的所在位置进行传值;
注意:不能在开头使用send,因为没有上一个yield,也不能在最后一个yield使用send,因为已经到结尾了;
def generator(): print(123) content = yield 1 print('=======',content) print(456) yield2 g = generator() ret = g.__next__() print('***',ret) ret = g.send('hello') #send的效果和next一样 print('***',ret) #send 获取下一个值的效果和next基本一致 #只是在获取下一个值的时候,给上一yield的位置传递一个数据 #使用send的注意事项 # 第一次使用生成器的时候 是用next获取下一个值 # 最后一个yield不能接受外部的值
def averager(): total = 0.0 count = 0 average = None while True: term = yield average total += term count += 1 average = total/count g_avg = averager() next(g_avg) print(g_avg.send(10)) print(g_avg.send(30)) print(g_avg.send(5))
yield from
def gen1(): for c in 'AB': yield c for i in range(3): yield i print(list(gen1())) def gen2(): yield from 'AB' yield from range(3) print(list(gen2())) --------------- 输出打印 ----------------------------------------- ['A', 'B', 0, 1, 2] ['A', 'B', 0, 1, 2]