点击 生成器本身属于迭代器。继承了迭代器的特性,惰性求值,占用内存空间极小。
为什么要有生成器我们想使用迭代器本身惰性求值的特点创建出一个可以容纳百万级别的迭代器。(节省内存)又不想通过调用可迭代对象下的__iter__方法来创建这样的迭代器。(未经过优化的可迭代对象本身就占据内存,如list,tuple,dict,set,str等)这个时候就需要用到生成器。
怎么用生成器定义生成器的方式有两种。
.使用生成器表达式(本章不做介绍)
2.使用生成器函数
斐波拉契数列的创建:
#====使用yield创建生成器====importarray#数组,只能存放单一数据类型。如果要存放同一类型的数据,它比列表更好用s=array.array("I")deffblq(n):x,y,z=0,0,whilexn:yieldy#相当于return,暂停在此处。再次调用时继续执行下面的代码y,z=z,y+zx+=g=fblq(0)print(g)#现在的g是一个生成器对象.generatorobjectfblqat0xF2C6EDEforiing:print(i)#取出一个,计算一个。s.append(i)print(s)#array(I,[0,,,2,3,5,8,3,2,34])
惰性求值:需要用的时候经过内部计算取出一个。而不是管你目前用不用得到全部给你取出来。
生成器总结优点节省内存。不用通过创建一个未经优化的可迭代对象再调用其__iter__方法创建迭代器。
缺点由于生成器本身就是属于迭代器。故缺点是只能使用一次,当值全部取出后该生成器对象意味着死亡。如果生成器要取中间的值,只能通过一个一个的迭代过去。不能直接取出中间的值。
故:Python对于list,dict等数据类型为何不直接采取引用迭代器的方式呢?这是因为Python考虑到其还有其他的取值方式。如index,key等等取值要比遍历取值更为方便。
引用迭代器(经过优化的可迭代对象):
可迭代对象本身并不存储任何值,for循环该可迭代对象时实际上就是生成一个迭代器,再通过该专属迭代器的next方法内部计算出需要的值并且返回。
这么做的方式在于不能通过index取值,但是极大节省内存空间。采用引用迭代器方式的数据类型有很多,比如:keys(),values(),items(),range()。
扩展:生成器与协程生成器由于具有挂起当前函数状态的特性,所以可以有很多骚操作玩法,也间接的让协程成为可能。我们可以让一个生成器函数做不同的事情,根据不同的情况返回不同的结果。
需要注意,yield本身具有return返回值的功能。并且还有接收值的功能。
yield返回值所接收的对象将获得两个方法:
send()---向yield发送一个任意类型参数。
close()---当使用该方法后,将不再具有send()方法。
#====生成器的send与close====defdog():#等待send(None)或者next(host)执行。print("dog的绳子被主人拉上了..")a=yield"dog饿了"#返回值,相当于狗对人说的话。a相当于外部第二次send进来的值print("dog吃了一坨",a)b=yield"dog渴了"print("dog喝了一口",b)yield"dog吃饱喝足了"host=dog()msg=host.send(None)#第一次启动必须是None。或者使用next()开始生成器的执行。print(msg)msg=host.send("冰淇淋")#对于send来说。内部有几个yield外部就该有几个sendprint(msg)msg=host.send("82年的雪碧")print(msg)#====执行结果===="""dog的绳子被主人拉上了..dog饿了dog吃了一坨冰淇淋dog渴了dog喝了一口82年的雪碧dog吃饱喝足了"""扩展:函数状态挂起底层原理
Python中生成器函数是一个非常牛逼的东西。它可以让函数挂起状态,那么底层到底是怎么实现的呢?
原文链接: 另外推荐深度好文: 这种在函数内调用另一个函数的方式类似于递归,我们可以看一张图:
#我们之前说了,栈帧是分配在堆内存上的#正是因为如此,生成器才有实现的可能#我们定义一个生成器defgen_func():yield23name="satori"yieldage=8return"ilovesatori"#注意在早期的版本中生成器是不允许有返回值的,但在后来的版本中,允许生成器具有返回值#python解释之前,也进行预编译,在编译的过程中,发现有yield,就已经被标记为生成器了
defgen_func():yield23name="satori"yieldage=8return"ilovesatori"importdisgen=gen_func()print(dis.dis(gen))0LOAD_CONST(23)YIELD_VALUEPOP_TOP6LOAD_CONST2(satori)STORE_FAST0(name)0LOAD_CONST3()YIELD_VALUEPOP_TOP6LOAD_CONST4(8)STORE_FAST(age)20LOAD_CONST5(ilovesatori)RETURN_VALUENone#可以看到,结果中有两个yield,因为我们的函数中有两个yield#最后的LOAD_CONST后面的(ilovesatori),表示我们的返回值#最后RETURN_VALUE#前面的图也解释了,gi_frame的f_lasti会记录最近的一次执行状态,gi_locals会记录当前的局部变量print(gen.gi_frame.f_lasti)print(gen.gi_frame.f_locals)-{}#我们创建了生成器,但是还没有执行,所以值为-,当前局部变量也为空#我们next一下next(gen)print(gen.gi_frame.f_lasti)print(gen.gi_frame.f_locals){}#我们发现数字是2,所以指向第二行,YIELD_VALUE,yield的值就是23#此时局部变量依旧为空#继续next,会执行到第二个yield的位置next(gen)print(gen.gi_frame.f_lasti)print(gen.gi_frame.f_locals){name:satori}#数字是2,所以指向第十二行,第二个YIELD_VALUE,yield的值就是#此时name="satori",被添加到了局部变量当中#因此到这里便更容易理解了,为什么生成器可以实现了。#因为PyGenObject对函数的暂停和前进,进行了完美的监督,有变量保存我最近一行代码执行到什么位置#再通过yield来暂停它,就实现了我们的生成器#跟函数一样,我们的生成器对象也是分配在堆内存当中的,可以像函数的栈帧一样,独立于调用者而存在#我们可以在任何地方去调用它,只要我们拿到这个栈帧对象,就可以控制它继续往前走#正是因为可以在任何地方控制它,才会有了协程这个概念,这是协程能够实现的理论基础#因为有了f_lasti,生成器知道下次会在什么地方执行,不像函数,必须要一次性运行完毕#以上就是生成器的运行原理
生成器的栈帧与普通函数的栈帧并不相同
扩展:自定义序列实现迭代器#====自定义序列实现迭代器====classMy_list(object):"""为了能让该容器能够多次被循环调用,故做成可迭代对象。每次for循环为其创建一个专属迭代器。"""def__init__(self,*args):self.args=argsdef__iter__(self):returnMy_list_iterator(self.args)classMy_list_iterator(object):def__init__(self,args):self.args=argsself.index=0def__iter__(self):returnselfdef__next__(self):try:return_value=self.args[self.index]exceptIndexError:raiseStopIterationself.index+=returnreturn_valueif__name__=="__main__":#for循环原理。#.创建专属迭代器。#2.不断执行next方法。#3.捕捉StopIteration异常l=My_list(,2,3,4,5)l_iterator=iter(l)whileTrue:try:print(next(l_iterator))exceptStopIteration:break
注意:即使没有__iter__方法。只要对象具有__getitem__也是可以间接的创建专属迭代器。但是效率偏慢。
扩展:range()方法的返回值与优化range()返回的是一个可迭代对象,但是range()这个可迭代对象并不像list那种可迭代对象一样真正占据内存空间。当for循环对其遍历的时候通过range()返回的可迭代对象本身__iter__方法创建出一个专属的迭代器。然后其专属迭代器中的__next__方法里面是通过计算结束和步长的关系达到惰性求值的效果,range()的__iter__方法创建出的迭代器并不属于生成器范畴但是有着和生成器异曲同工的作用。
一句话总结:range()方法返回的可迭代对象并不存储具体的值,但是要对其进行遍历时创建的专属迭代器是具有惰性求值的特点的。我将它称为优化后的可迭代对象,注意这个优化只是针对内存空间中的优化。但是它也有不方便的地方,就是不能通过index取值!!!
#另外附上群中大佬自己写的一个Range#惰性求值,并且将本身做成了一个可重复调用的迭代器。#总结:很强!!!classRange(object):def__init__(self,scale,s=0,d=):self.s=s#开始self.e=scale#总长度self.d=d#步长def__iter__(self):self.i=self.s#开始值returnselfdef__next__(self):ifself.iself.e:x=self.iself.i+=self.dreturnxelse:self.i=self.s#self.i=0raiseStopIteration#尽管Pyhon3中的range并不是直接返回一个迭代器本身。但是大佬的这种做法#依然很厉害,__next__中的惰性求值也是和生成器有着异曲同工之妙。而且大佬本身自己就做成了一个可重复使用的迭代器。原文