迭代器和生成器
迭代器
什么叫可迭代对象呢,就是遵循迭代器协议的对象叫可迭代对象,迭代器协议有哪些呢?
1:只可向后不可向前
2:必须具有__next__()方法
(以上都只是讲个大概,基本上就是这样,最重要的是第二点)
for循环是基于可迭代对象的next()方法来迭代的,也就是基于迭代器的协议来实现的,所有for循环和强大,强大到甚至连文件对象都可以迭代,可问题来了,你看下面的例子,l 这个列表可以被for迭代开始 l 这个列表并没有__next__()方法,这是怎么回事呢?这是否意味着列表不是可迭代对象呢? 答案:是的,列表元组字符串字典集合之类的全部不是可迭代对象
l = [1,2,3,4] for each in l: print(each) 结果 1 2 3 4
现在来讲一下原理,可迭代对象<==>迭代器对象,因为for循环非常强大,它在迭代列表元组字符串这些东西时,会调用列表元组字符串的__iter__()方法,生成一个对应的可迭代对象,就是迭代器对象,然后就可迭代啦,意思就是说只要能被for循环迭代的非可迭代对象必须有__iter__()方法,下面来测试一下
a = [1,2,3,4] s = a.__iter__() s.__next__() print(s) 结果 <list_iterator object at 0x0000018442E892E8>
//next()函数,这个系统内置的函数其实在调用时是调用了迭代器对象的__next__()方法
生成器
生成器是自动变成迭代器对象的,不需要认为的再调用__iter__()方法,有两种方式写生成器,1:生成器函数,2:生成器表达式
先说生成器函数,只要将我们平时写函数时的 return 改成 yield,有个小地方不一样的是,函数的return只可以一次,但是yield可以多次,每一次的yield相当于生成器函数的一个状态,而且生成器函数没有省内存这么一说,因为和普通函数比起来一样,都是函数所以都先存在内存之中,生成器表达式才有省内存这么一说,下面会详细讲,当然生成器函数也是有优点的,对于调用普通函数,是先将函数全部执行加载完之后再执行下面主程序的操作,但对于生成器函数而言,可以一边执行加载到内存一边用你目前加载到内存的数据,这样效率就高一点
def func(): yield 1 yield 2 yield 3 g = func() print(g) print(g.__next__()) print(g.__next__()) print(g.__next__()) 结果 <generator object func at 0x0000025CEBB7CFC0> 1 2 3 def func(): print('one time') yield 1 print('two times') yield 2 print('three times') yield 3 print('that is all') g = func()//生成器函数没有执行,只是返回一个生成器先,并且状态为最开始,生成器函数真正的执行是需要 __next__()方法的 print(g.__next__())//回到生成器的函数中从记录的状态开始执行,到第一个状态然后暂时挂起函数并记录下状态(也就是记录到执行完了yield 1 这条语句 yield相当于状态语句) print(g.__next__())//重新回到生成器函数中,从记录的状态开始执行也就是第一个状态之后(yield 1 这条语句之后),执行到第二个状态并记录下来挂起函数 print(g.__next__())//重新回到生成器函数中,从记录的状态开始执行也就是第二个状态之后(yield 2 这条语句之后),执行到第三个状态并记录下来挂起函数 //由于后面没有再继续调用生成器函数,所以 print('that is all') 这条语句没有被执行 结果 one time 1 two times 2 three times 3 母鸡生蛋传说 def func(): ret = [] for each in range(1000): ret.append('鸡蛋%s' % str(each+1)) return ret ret = func() people1=ret[0] print(people1) people2=ret[1] print(people2) 结果 鸡蛋1 鸡蛋2 //这个方法是怎么样呢,是先让母鸡将1000给蛋全部生完,然后拿个篮子装起来,然后再将篮子里的鸡蛋分给后面1000给正在等着领鸡蛋的人,那这样效率是不是低 换种方法,能不能母鸡生一个就分一个鸡蛋给正在等待的人,那是不是人排队领鸡蛋所需的时间就短了, 假设生一个鸡蛋要1s领一个鸡蛋要1s 对于第1000个人来说前两种方法都一样,都要 等2000s 但对前面的999个人来说,第一个人对于第一种方法需要1001s才领完,对于第二种方法只需要2s,所以对于前999个人来说速度都变慢了,对于第1000个人来说速度没变,那这样 用第二种方法是不是效率就高了,而且还不需要篮子将1000个鸡蛋装起来,生一个给一个,根本不需要装起来(对于内存来说第一种方法就占内存,因为需要将数据全部存起来先,对于第二种 方法就不占内存,因为是生成器函数,调用一次__next__()方法就生成一个数据,调用第二次时生成第二个数据,第一次的数据已经处理完不需要存起来,一开始这个生成器函数是一个数据 都没有生成的,是当你调用生成器的__next__()方法时才开始生成数据,而且是调用一次生成一个),这样的话效率就会高一点 def func(): for each in range(1000): yield '鸡蛋%s' % str(each+1) return ret ret = func() people1=ret.__next__() print(people1) people2=ret.__next__() print(people2) 结果 鸡蛋1 鸡蛋2
//三元表达式,看例子自己回忆
name = 'djh' a = '帅哥' if name == 'djh' else 'sb' print(a) 结果 帅哥
//列表的解析式
s = [] for each in range(10): s.append("帅哥%s" % str(each+1)) print(s) 结果 ['帅哥1', '帅哥2', '帅哥3', '帅哥4', '帅哥5', '帅哥6', '帅哥7', '帅哥8', '帅哥9', '帅哥10'] 列表解析式可以很简单的达到目的 s = ['帅哥%s' % str(each+1) for each in range(10)] print(s) 结果 ['帅哥1', '帅哥2', '帅哥3', '帅哥4', '帅哥5', '帅哥6', '帅哥7', '帅哥8', '帅哥9', '帅哥10'] 还可以这么玩 s = ['帅哥%s' % str(each+1) for each in range(10) if each >5] print(s) 结果 ['帅哥7', '帅哥8', '帅哥9', '帅哥10']
现在说一下生成器表达式,自己感受啦,其实就是列表解析式的格式,只不过因为返回的是有个生成器不是一个列表所将以 [] 变成了 () 而已
s = ('帅哥%s' % str(each+1) for each in range(3)) print(s) print(s.__next__()) print(s.__next__()) print(s.__next__()) 结果 <generator object <genexpr> at 0x000002339848CFC0> 帅哥1 帅哥2 帅哥3
需要强调的一个地方是,用生成器省内存,以生成器表达式为例子,生成器表达式都是在调用到某个值时才生成这个值,没调用到时是不生成的,并且在使用结束之后由于没有变量指定该值,会被python的回收机制给回收,所以根本不占内存,但是如果是用列表的形式的话,它会先在内存中将该列表的所以元素给存起来先,然后调用时直接调用,这样就很占内存,下面举例说明
print(sum(list(range(1000000000000000000)))) sum()函数的工作原理也给予迭代器协议工作的,第一步先在内存里生成 list(range(1000000000000000000)) 这么大的一个列表,将其每个元素全部存在内存中先,这样就占用了十分 巨大的内存,甚至可能机器卡死,第二步再生成 list(range(1000000000000000000)) 的迭代器对象,当然这步不占内存,但如果换个写法 print(sum(each for each in range(10000000000000000))) 你看,传入的是一个生成器,过程是这样的,sum()函数是把全部元素加起来嘛,当它要加第一个元素时,会去找这个 生成器,也就是这个each for each in range(10000000000000000))表达式,生成第一个数(我猜测可能还会有个负责记录的变量,记录到目前为止已经生成到哪个了),当加第二个 数时它又会去找回这个each for each in range(10000000000000000))表达式生成第二个数,第一个数用完了并且没有变量去指向所以会被回收,后面的以此类推,那这个过程就不像 上面的那个方法先在内存里把全部元素存起来那么费内存
//send()方法,有一个参数,必填,是专属于生成器的方法,迭代器没有这个方法,send()方法其实主要是用在生成器函数里的,它是调用生成器函数让其继续往下走,并且将其参数的值返回给生成器函数里的yield。
def func(): print("the fist") fist = yield 1 print(2,fist) second = yield a = func() res = a.__next__()//之所以一开始不用send()方法是因为一开始状态并没有停在yield语句那里,所以send()的参数不知道给谁接收所以一开始都是先用__next__()方法 print(res) res = a.send(None)//将send()的参数返回给当前状态停靠的那个yield(也就是yield 1),然后将当前的yield赋值给fist print(res) 结果 the fist 1 2 None None
下面给个单线程模拟实现并发的程序,就是两个独立的程序之间不需要人为的显示的调用就可以跳来跳去。
举例吃包子,做包子和吃包子应该是并行的两个程序对吧,你见过哪家店是把包子全部做完然后再卖给食客吃的,全都是一边做一边卖
import time def consumer(name): print("%s已经进入餐馆准备放开肚皮吃包子了" % name) while True: baozi = yield None time.sleep(0.5) print("%s已经吃了包子%s" %(name,baozi)) def product_baozi(): c1 = consumer('djh') c2 = consumer('DJH') c1.__next__() c2.__next__() for each in range(10): time.sleep(0.5) c1.send(each) c2.send(each) product_baozi() 结果 djh已经进入餐馆准备放开肚皮吃包子了 DJH已经进入餐馆准备放开肚皮吃包子了 djh已经吃了包子0 DJH已经吃了包子0 djh已经吃了包子1 DJH已经吃了包子1 djh已经吃了包子2 DJH已经吃了包子2 djh已经吃了包子3 DJH已经吃了包子3 djh已经吃了包子4 DJH已经吃了包子4 djh已经吃了包子5 DJH已经吃了包子5 djh已经吃了包子6 DJH已经吃了包子6 djh已经吃了包子7 DJH已经吃了包子7 djh已经吃了包子8 DJH已经吃了包子8 djh已经吃了包子9 DJH已经吃了包子9