先上完整代码,
接下来是控制台里的代码结果输出,
下面是,为了更好的跟踪程序的执行,在代码中插入多个print()后,在控制台的输出
第一阶段:
瀑布下流
第二阶段:
逆流而上,
可以看出,自下而上,从yield nestedList 开始,向上返回了 2 次,所以,三个等号被打印了 2 次,每次返回都触发了 3 个打印的 语句
所以有了 的结果
这里只是粗略的画了2个图,希望大家能看懂,以后有时间我再来继续完善这篇文章,
October the 28th 2022 Friday ,
再补充几点,
1.递归(recursion / recurrent)就是有去有回,你调用自己就相当于出去了,那么在完成了任务之后,还得回来,这就是我的理解,
2.生成器是一种特殊的迭代器,所谓迭代器,就是可以遍历的容器,比如:for i in range(10):
就会得到 0~9 十个值,那么range(10)就是一个可以被迭代的容器,之所以叫做容器,是因为容器里面可以装很多东西,在编程里,容器就装了很多个元素(值),range(10)是一个迭代器,可以使用 for 语句 遍历并输出 range(10)这个迭代器里的所有元素(值),
3.生成器相较于迭代器特殊的地方在于,生成器内部的语句可以被yield暂停,直到生成器被再次唤醒,生成器内部的语句才会接着上次暂停的地方继续向下执行,
4.生成器另一个特殊的地方在于,生成器是从生成器模板来的,而生成器模板是一个包含yield语句的函数(方法),
举个例子:
def enumList(nestedList):
try:
for subList in nestedList:
for element in enumList(subList):
yield element
enumList,看起来是用 def 关键字 定义的一个函数,但实际上,一旦一个函数内部包含了yield语句,它就不再是一个函数了,而是
生成器的模板,所以,enumList 就是一个生成器的模板,
而 enumList(nestedList) 不是调用函数,而是用 生成器模板 enumList 做出了一个生成器 ,使用 enumList(nestedList) 不是很方便
所以,通常将做出的生成器 赋值给一个变量,例如:
enu=enumList(nestedList) enu就变成了一个生成器了
print(enu,type(enu))
===》输出结果是:
<generator object enumList at 0x000001FAE6276430>
<class ‘generator’>
可以看到,enu 是 生成器 即 generator ,且 enu 是一个具体的 生成器的一个 实例,有内存地址 at 0x000001FAE6276430
5.yield 作用是,
(1)暂停程序,也就是它下面那行开始的所有语句都停止执行,
(2)同时,将它后面的 值 给 在等待这个值的 接收者 送过去,一个有缘人,而在本文中,接收 yield 后面跟着的那个值的 始终是 element,这个变量。
举个简单的例子来理解一下yield 作用,和本文的难点
for element
in enumList(subList)
:
1.element
想从生成器 enumList(subList)
获得一个值,上面我们说过了,enumList(subList)
它不是在调用函数enumList(subList)
,因为enumList
中包含了yield语句,所以它就不再是一个普通的程序了,而是变成了一个生成器的模板,enumList(subList)
是做出了一个生成器,
那么,这个生成器里就应该有一些值(元素),所以,for element
in enumList(subList)
:的意思是,element
想从生成器 enumList(subList)
获得一个值,
2.但是,生成器enumList(subList)
得根据 subList 提供的东西 ,看人下菜碟的给 element
想要的值
def enumList(nestedList):
try:
for subList in nestedList:
for element
in enumList(subList)
:
yield element
except TypeError:
yield nestedList
经过3次递归,即在第3次递归的时候,nestedList变成了1,而 for subList in nestedList: 是错误的,因为 1 不能被迭代,所以会报
TypeError,所以就被 except捕获, 进入了 except TypeError:代码段中,执行,yield nestedList ,即返回 nestedList 的值,
我们知道,在这次生成器enumList(nestedList)的执行中 nestedList =1,所以,yield 把1 送出去,并暂停 本次生成器enumList(nestedList)其他语句的执行,
那么关键的地方来了,这个1 送给谁了,结合上文,我们就清楚了,是element在等着这个值,
我们再捋一下,
在第3次递归的时候,也就是,执行 def enumList(nestedList): 的时候,由于 1 是不可以被迭代的,所以,程序 进入了except 代码块中,并最终返回了 nestedList 当时的值 1,也就是说,执行 def enumList(nestedList): 的结果是 得到了一个1,
那么这个 1 就被送到了 第二次 递归语句中的 element ,那么,因为element拿到了值 1,所以会执行 yield element 语句,所以,element 的值 1 被yield 送到了 第一次递归 语句中的 element 处 ,所以,第一次递归中 的 element 也拿到了 它想要的值 1,所以会 顺序执行 其下的语句 yield element,而,这是最外面的递归程序了,所以,我们就能看到 通过 print(next(enu),‘01st call’) 调用 生成器 enumList(nestedList)得到最终的结果 1 01st call
先这么着吧,面要陀了,以后有时间我再来补充完善。
接着说,
13:22 22/10/28
刚才继续看代码,发现,上面落下了一个非常非常重要的点,
就是,yield 所处的位置,对于代码的运行是有着至关重要的作用的,
这里就不画图了,用文字干说,
1.先看第三次递归中,
def enumList(nestedList):
try:
for subList in nestedList:
for element in enumList(subList):
yield element
except TypeError:
yield
nestedList
第三次 递归中,yield 返回的是 nestedList, yield
所处的位置是程序的最末一行,因此,第三次递归中所有的语句都执行完毕了,所以,当再次 next()的时候,程序不会在第三次递归中继续运行,而是会在第二次递归中找 yield
2.接着看第二次递归,
def enumList(nestedList):
try:
for subList in nestedList:
for element in enumList(subList):
yield
element
except TypeError:
yield nestedList
第二次递归,返回的是 element ,yield
所处的位置是,内层 for 语句块中,在被next() 语句激活后,按道理会接着执行 for element in enumList(subList):的第二次循环,但,上面第三次递归中,只能返回一个值,所以,内层 for 语句没有东西可以执行了,因此就退出了 内层 for 语句,进行外层 for语句的 第二次循环,我们知道,在第二次递归中,nestedList=[1,2,3],所以,第二次递归中 外层 for语句的第二次循环,subList 拿到了2这个值,于是进入 第三次递归中,for subList in nestedList:
报错,因为此时 nestedList=2,所以进入第三次递归中的except TypeError:语句,遇到 yield nestedList,(1)冻结程序,(2)将 nestedList=2 返回给第二次递归中的 element ,第二次递归中的element 有值了为2 所以可以继续执行 内层 for语句的 yield
element 语句,1)冻结程序,(2)将 element =2 送到第一次递归 中的 element ,第一次递归 中的 element 拿到了 值2 ,就 具备条件 可以继续执行 内层 for语句 的 yield
element 语句 ,1)冻结程序,(2)将将 element 的值 2 送出,由于这时已经是最外面的递归了,所以 element 的值 2 送出后,被 next()接收,完成第二次 next()调用
所以, 本篇文章的精华是,第二次 next()调用,因为它要接着所有的yield
去执行,而有的yield
处于 for 循环中( yield
element),所以就必须接着上次的循环开始下一次循环,而有的 yield
不在 for 循环中而是处于末尾的位置,因此没有什么可做的话就会自然结束工作,就是说完成了对这个 yield
的处理,暂时不用管它了,接着去处理其他的 yield
就好了。
另外,也要注意,递归是有层次的,每次递归时,都会有yield参与其中,处理的方式方法是不同的,
1.递归的层次,2.yield的位置,这两点搞清楚了,一切就迎刃而解了。
豁然开朗。
13:57 22/10/28
继续补充,我又想到了一个好的理解方式,
拿 第一次 的 next()举例
for element in enumList(subList)
:
print(‘===’)
print(‘15 line element=’,element)
yield element
我们知道 element 是要 朝 enumList(subList)
要东西的, 而 第三次递归的结果只有一个 1,也就说 ,在第二次递归中, enumList(subList)
只有一个元素 1,所以 在第二次 next() 虽然激活了 第二次递归(每次next()调用都会有2到3次递归,这里不要糊涂了)中的 yield ,但由于此时的 for 语句 里的 enumList(subList)
只有一个 值 1,所以就没有新的循环开始,因此,虽然 此处的 yield 在 for 语句中,也没法开始下一次循环, 因此,内层 的 for 也就无奈的只好结束了。退出内层 for 语句 回到 外层的 for语句中 ,做该做的事情。
15:13 22/10/28
继续补充
November the 03rd Thursday 2022
今天继续来补充,相信这次可以说明白这个问题,如果,有同学不想被误导,上面的可以直接忽略,从这里看就行。
首先,
第一个重点是:对yield
语句的理解,当遇到 yield
语句,它会做两件事,
1.暂停 yield
所在的程序段所有语句的执行,相当于冻住,
2.将yield
后面的变量值送出去,谁调用了包含yield
的代码块,就送给谁,
第二个重点是:对递归
的理解,递归
就是自己调用自己,是有层次的,最外层在等待它内部的那层自己的返回值,可以看做是俄罗斯套娃,从外向内时,只有将外层套娃打开,才能看到内层的套娃,以此类推,从内向外时,只有将内层的套娃处理完包好,才能继续处理外层的套娃,以此类推。
所以递归的时候,总的来看是:外面的程序等待里面的程序给出结果,
换言之:只有里面的程序有了结果,才能接着处理外面的程序,
知道这个就够用了。
其次,
拿上面这段‘将多维列表展平’的程序来说,还需要注意的一点是:递归
和yield
搭配一起使用的时候,每层递归
里都有yield
,所以,每层递归
与其他层的yield
是不同的,就像要处理每层递归
一样,也要按顺序逐个处理每层递归
中的yield
,
yield
是有顺序的,每次调用 next() 的时候,都要顺序处理上次调用 next()时遇到的各个yield
,
处理的顺序是:本次调用 next()要最先
处理上次调用 next()时遇到的第一
个yield
,然后本次调用 next()才应该去处理,上次调用 next()时遇到的第二
个yield
,也就是说,每次调用 next()时,都要首先处理上次调用时遇到的最早的那个yield
,看起来的顺序是从内往外处理yield
,
最后,
要注意的是每次