目录
结论:
1:可迭代对象:
2:生成器:概念如下:
3:迭代器的定义:要同时满足以下三点
一:可迭代对象的分类
二:迭代器的意义和应用场景
1:迭代器的意义
2:迭代器的应用场景
a:用迭代器来构建数据管道
b: 数据生成器:但并不等同于yield所返回的生成器
三:生成器
1:什么是生成器
a: 只能用在函数内;
b:在函数内任何一个地方出现了,yield,那么哪怕永远无法被执行到,函数都会发生变异;
2:yield是语句还是表达式
3:生成器的4个状态
a:当调用生成器函数得到生成器对象的时候,此时的生成器对象可以理解为初始状
b:通过next()调用生成器对象,对应的生成器函数代码开始运行;
c:如果遇到yield语句,next()返回时:
d:如果执行到函数结束,则抛出stopIteration异常:
4:用yield重构迭代器
四:生成器背后的运行机制
1:生成器函数与普通函数之间的区别
a:普通函数
b:生成器函数
五:由此引出同步和异步的关系
六:协程
本文主要参考B站系列文章:明明是生成器,却偏说是协程,你是不是在骗我? | Python AsyncIO从入门到放弃04_哔哩哔哩_bilibili
结论:
1:可迭代对象:
若一个类含有__iter__方法并且__iter__方法返回的是一个迭代器对象,则称这个类为可迭代对象,也就是说,这个类本身不含有__next__方法,但是__iter__返回的对象是含有的;同时,其实我们说可迭代对象的__iter__方法返回一个生成器对象也是可以的,因为生成器是一种特殊的迭代器,见下文;
补充:for循环其实就是先通过__iter__方法返回一个迭代器对象,然后不断调用这个迭代器的next获取值,同时,for循环再找不到__iter__的时候,也可以找__getitem__,详细的参考本系列另一篇文章,定义__getitem__方法的类,也是可迭代的;
举例:比如python中的range()方法就是一个可迭代对象,她含有__iter__方法,并返回一个迭代器,但本身不含有__next__方法,所以a = range(1000),这里的a其实就是含有__iter__的可迭代对象,此时用b = a.__iter__(),则b就是一个迭代器,分别用dir()函数去查看a和b的属性可以直观看到两者的差别;
2:生成器:概念如下:
a:如果一个函数含有yield关键字,那么这个函数叫做生成器函数;
b:生成器函数返回的是生成器对象(generator),也就是说yield返回的是生成器对象,所以yield真实的操作是调用了python内置的generator类创建了一个对象并返回,而这个generator类本身也声明了__iter__和__next__方法,所以,我们可以说生成器其实属于迭代器的一种;
3:迭代器的定义:要同时满足以下三点
a: 含有__iter__和__next__内置函数的类;迭代器必须同时实现这两个方法,这称之为迭代器协议;那根据这个协议,我们可以知道,迭代器一定是可迭代对象;
b: __iter__内置函数要返回自身self;
c: __next__方法返回下一个数据,如果没有数据了,则需要抛出一个stopIteration的异常
补充:迭代器的意义:
参考:15分钟彻底搞懂迭代器、可迭代对象、生成器【python迭代器】_哔哩哔哩_bilibili
一:可迭代对象的分类
参考自:Python 迭代器深入讲解 |【AsyncIO从入门到放弃#1】_哔哩哔哩_bilibili
大致分为两类,一类是容器类型的(只含有__iter__),一类的迭代器类型的(同时含有__iter__和__next__)
这里的只能迭代一次,是指一次迭代完以后无法从头再来;比如列表,袁组这类的容器类型的可迭代对象,是可以多次迭代的,每次从头迭代都可以,但是迭代器类型只能迭代一次,比如range只能生成一个;
二:迭代器的意义和应用场景
1:迭代器的意义
2:迭代器的应用场景
a:用迭代器来构建数据管道
b: 数据生成器:但并不等同于yield所返回的生成器
三:生成器
1:什么是生成器
我们把含有yield关键字的函数称为生成器函数,把调用生成器函数返回的结果称为生成器;生成器对象是迭代器,那么就必须满足上面迭代器的要求;
先说yield关键字:其有两个特点
a: 只能用在函数内;
b:在函数内任何一个地方出现了,yield,那么哪怕永远无法被执行到,函数都会发生变异;
这里所谓的变异是说,当你执行这个函数的时候,这个函数将不再直接运行,如下图,执行g = gen()的时候,并没有打印hello,也就是说这个函数并没有运行,二是返回了一个对象g,因为正常来说函数运行直接gen()就可以了,是没有返回值的。而这里返回了一个值,这个值是是一个generator对象;
再细看的话,生成器函数依然还是属于函数,不是生成器;
2:yield是语句还是表达式
yield关键字到底是语句还是表达式呢?最开始是语句,后来升级为表达式了;为什么要升级呢?因为为了实现协程,因此需要再原来的生成器基础上实现一个增强型的生成器;
这里我们先讲第一个,也就是作为语句时候的yield,可以看作是return;
yield关键字最根本的作用是改变了函数的性质:包括以下两点
a:调用生成器函数不是直接执行其中的代码,而是返回一个生成器对象;
b:生成器函数内的代码,需要用过生成器对象来执行;
因此,从这一点来说,生成器函数的作用和类是差不多的。
我们又说生成器对象实际上就是迭代器,所以其运行方式和迭代器是一致的:
a:通过next()函数来调用;
b:每次next()都会再遇到yield后返回结果,作为next()的返回值;
c:如果函数运行结束(即遇到了return),则抛出stopIteration的异常;
举例如下:
当不经过yield的时候:
这里遇到return,直接出发stopIteration异常,实际上,return在生成器里面的作用就是出发stopIteration这个异常,而且return的结果讲作为异常返回值被打印出来,如上图所示;
当经过yield的时候:
可以看到,yield的值是直接返回了的,而且此时函数停在yield语句出,当使用next语句的时候,代码将从这里继续运行;
3:生成器的4个状态
a:当调用生成器函数得到生成器对象的时候,此时的生成器对象可以理解为初始状
生成器函数本身的内容也不会执行;
b:通过next()调用生成器对象,对应的生成器函数代码开始运行;
此时生成器对象处于运行中状态;
c:如果遇到yield语句,next()返回时:
a:yield语句右边的对象作为next()的返回值;
b:生成器在yield语句所在的位置暂停,当再次使用next()时继续从该位置运行;
d:如果执行到函数结束,则抛出stopIteration异常:
a:不管是使用了return语句显示的返回值,或者是默认的返回None值,返回值都只能作为异常的值一并抛出;
b:此时的生成器对象处于结束的状态;
c:对于已经结束的生成器对象再次调用next(),直接抛出stopIteration异常,并且不含返回值;
4:用yield重构迭代器
与和class定义的迭代器对比如下:
四:生成器背后的运行机制
主要包含下面三个内容:
- 生成器函数和普通函数之间的区别
- 生成器对象和生成器函数之间的关系
- 生成器函数可以“暂停”执行的秘密
1:生成器函数与普通函数之间的区别
a:普通函数
先说普通函数的运行机制,每当定义一个函数之后,我们就得到了一个“函数对象”,但实际上函数中的代码时保存在"代码对象"中的;
这里,再结合自己的实验展示如下:
代码对象随着函数对象一起创建,是函数对象的一个重要属性,代码对象中重要的属性都是以co_开头,如上图所示;
但实际上,这个时候函数对象和代码对象只是保存了函数的基本信息,当函数运行的时候,还需要一个对象来保存运行时的状态,这个对象就是“帧对象”;每一次调用函数,都会自动创建一个帧对象,记录当前运行的状态;
可以利用inspect函数来得到函数的运行帧,通常情况下,这个帧对象它在函数运行结束的时候会被自动销毁,也就是被垃圾回收,但为了演示,这里将其保存为变量f1和f2如下:
这里用一个辅助函数show_backrefs来展示函数对象,代码对象和帧对象之间的关系如下:
code这里是代码对象,function这里是函数对象,frame这里是帧对象 ;从图上可以看到,函数对象和帧对象都对代码对象有引用;
帧对象中的重要属性都是以f_开头:
这里就引出了函数运行栈的概念,因为当一个函数调用另一个函数的时候,前一个函数还没有结束,所以这两个函数的帧对象是同事存在的。 所以,一个程序的运行期同时存在着很多个帧对象。
另外需要补充一点:一个线程只有一个函数运行栈;
ok,那什么是函数运行栈呢?
如下图所示:
首先f1是bar返回的foo()函数,所以此时最上面的帧对象引用的代码时foo函数的代码,如最顶上frame的f_code所示,此时这个最顶层的frame其实是有上一个frame引用的,这个上一个frame就是bar函数的帧对象,我们可以看到第二个frame的f_code其实是bar函数;以此类推,bar呢这里又是有jupyter的帧调用的等等,这样下来,左边这一长串frame就是函数运行帧了;
b:生成器函数
首先,生成器函数仍然是函数对象,当然也包括代码对象;但是,调用生成器函数不会直接运行(也就是说,调用生成器函数的时候不会像普通函数那样创建一个帧对象,并将这个帧对象压入函数栈)
关于帧对象到底在哪里,其实是保存在一个由python创建的全局帧栈中,也就是说从代码运行开始,python就创建了一个栈,然后不断将正在运行的代码的针对像进栈出栈,也就是函数不断运行;
参考:深入理解Python函数调用和栈帧-皮蛋编程 (pidancode.com)
我们来看为什么调用生成器函数的时候不会直接运行:
如图:调用生成器函数,返回一个生成器generator,这个生成器有一个gi_frame属性,这个属性保留了一个帧对象frame的属性,这个帧对象和普通函数的帧对象差不多,保留了对生成器函数的代码对象的引用;
所以,为什么调用生成器的时候没有直接运行生成器函数的代码,因为生成器这里自带了一个帧,这个帧与普通函数的帧不同,普通函数的帧对代码的引用是通过f_code,而生成器这里则是用的gi_code,如上图所示;
也就是说,每次用next来对生成器进行迭代的时候,都是用这个帧对象gi_frame来保存状态,这个帧对象其实是没变的,这就是为什么生成器可以暂停,所谓的暂停就是因为它把这个帧给保存下来了,因为我们一般的函数运行完以后,整个函数的帧对象就出栈了;
我们看一下接下来这个例子:
可以看到gfg是一个生成器对象,此时由于func_a调用了next函数,所以相当于此时先把func_a的帧对象压入了python的帧栈,再把gfg的帧对象压入了帧栈, 因此此时的栈顶是生成器的帧对象,而生成器的帧对象gi_frame所指向的帧其实是生成器函数的帧(也就是前面说的,gi_frame实际上保存了生成器函数的frame),因此每次next生成器的时候,实际上都是去通过生成器的帧对象gi_frame去调用了生成器函数的帧对象,实验如下,上图是func_a调用生成器,再次用func_b调用生成器,结果是一样的,只不过此时生成器帧对象gi_frame指向的生成器函数的帧对象frame的上一级帧对象是func_b的帧对象;
另外可以看到不论是func_a的帧对象还是func_b的帧对象都有一个箭头指向了生成器,其实就是简单指明是一个引用关系;
那么,此时我么也可以理解yield的用处了,其实就相当于将生成器帧对象gi_frame指向的生成器函数的帧对象出栈,当迭代结束的时候,则将生成器的帧对象gi_frame也出栈;
总结如下:
五:由此引出同步和异步的关系
普通函数的调用:
- 调用函数:构建帧对象并入栈;
- 函数执行结束:帧对象出栈并销毁
因此如果想要用普通函数来同时运行多个任务,只能采用同步的方式,如下,任务b想要执行,必须等到任务a执行完毕才可以:
而生成器函数的调用则不同
- 其先创建生成器函数帧对象frame,再创建生成器,构建生成器帧对象,并将生成器帧对象gi_frame指向生成器函数帧对象frame;但是,此时的生成器帧对象gi_frame并没有入栈;
- 其后,多次通过next出发执行,并将生成器帧对象入栈;
- 然后,每次遇到yield,则执行生成器帧对象出栈,但是并不销毁;
- 最后,当迭代结束的时候,生成器帧对象出栈并销毁;
这里,由于第2,3步时多次的出栈入栈,所以在2,3步之间,就可以由其他函数的帧对象进栈入栈; 于是,生成器函数就让异步运行任务变成了可能;
所以,我们现在可以进一步去理解生成器函数所返回的生成器对象是什么了,其实生成器对象就是一个用来迭代执行生成器函数的迭代器;
再进一步:
我们可以把迭代器分成两种:
一种是前面所说的数据的迭代器,针对一个包含很多元素的数据集,逐个返回其中的元素;
一种是这里所说的生成器迭代器,针对一个包含很多代码片段的函数,分段执行其中的代码;
那么基于上述的观点,我们说,当我们用生成器来实现迭代器的时候,如果我们的关注点是yield value所返回的value,那么,我们就将其理解为一个生成器就可以了,但如果,我们的关注点是集中在被迭代执行的代码上的时候,就应该对生成器有一个全新是视角,那就是协程;
六:协程
参考:明明是生成器,却偏说是协程,你是不是在骗我? | Python AsyncIO从入门到放弃04_哔哩哔哩_bilibili
未完待续......