神秘而强大的Python生成器精讲

一、 生成器(generator)概念
生成器是一种保存算法的特殊迭代器,每次调用next()或send()计算下一个元素的值,直到计算最后一个元素,没有更多的元素,抛出stopiteration。生成器有两种类型,一种是生成器表达式(也称为生成器推导),另一种是生成器函数。
二、 生成器表达式
生成器表达式是通过Python表达式语句计算一系列数据,但生成器定义时数据没有生成,而是返回一个对象,只有在需要时才根据表达式计算当前需要返回的数据:
生成器表达式来自迭代和列表分析(列表分析后一章)的组合。生成器类似于列表分析,但它使用小括号而不是中括号。生成器返回按需生成结果的对象,而不是一次构建结果列表;
生成器表达式语法如下:
(exprforiter_variniterable) (exprforiter_variniterableifcond_expr)
其中:
计算expr 生成器元素值的表达式
for iter_var in iterable iter_var:表示对可迭代对象iter_var中的每个元素进行表达式操作
if cond_exp:在参与表达式运算之前,可迭代对象中的元素需要满足指定的条件
说明
生成器表达式直接用于一对现有的小括号(如在函数调用中)时,不需要添加一对小括号。例如:sum(i ** 2 for i in range(10));
生成器表达式与列表分析的语法非常相似。由于涉及一些相关函数,老猿在列表分析相关章节中回顾了生成器表达式的相关内容。
三、 生成器函数
生成器函数是一种特殊的函数,在句子中包含yield关键字。它本身就是一个迭代器,需要通过调用next函数(或迭代器__来访问迭代器数据的代码next__方法)或send方法触发函数进行计算,并通过yield返回计算结果数据。返回数据后,函数立即停止执行,函数状态将保存在当地变量中,直到下一次外部调用激活,并从上一次停止执行部分开始执行。
1、 分析生成器函数和调用器的执行过程
生成器函数定义示意代码(非可执行代码)如下:
deffun():
初始化
循环:
计算得到k
nRet=yieldk
其它循环代码
上述代码表示:生成器函数运行时,计算结果k通过yield返回数据k给调用器。返回k给调用器后,生成器函数停止执行,yield的调用执行结果未返回给生成器函数。 nret的赋值没有执行。下次调用后,返回yield本身的执行结果,并继续执行后续循环代码,直到yield再次执行。
这里有几个细节可以通过验证来解释:
a) yield函数的执行是一个句子,但在实际执行中,句子被分解成两部分。第一部分是将计算结果k返回到send或next调用器(以下简称触发器),保存当前环境并暂停执行。另一部分是恢复当前环境,将yield本身的执行结果返回到生成器函数的调用器,并继续执行后续循环。每次调用yield时,除第一次从第一部分执行外,后续从第二部分执行。
b) yield返回值(nret记录的值)为next(含__next__方法,下同)为None,如果触发方为send,则该值为send方法参数中的发送值;
c) 生成器函数在调用时只生成一个生成器实例,并没有真正执行。只有当第一次通过next触发时,才会进入函数执行。请注意,第一次触发不能通过send触发。
2) 调用生成器代码示意
defmain():
初始化
f=fun() next(f)
循环:
其它循环代码
nRet=send(x)
其它循环代码
上述代码表示:调用器执行自己的初始化,然后对生成器函数进行初始化,然后对生成器函数进行循环迭代访问。
老猿也有几个细节在这里解释:
a) f= fun()这个句子不会进入函数执行,只会生成生成器实例f
b) 只有在循环代码中使用send触发时才需要第一个next调用。如果在循环中使用next,则无需执行send一次;
c) 第一个next将触发生成器函数的调用,从生成器的第一行代码开始;后续的next或send执行不再执行生成器函数的初始化部分,而只从yield的第二部分执行,第二部分应该在生成器函数的循环迭代码中执行,因此执行仍然在生成器函数的循环代码中循环,直到遇到yield句子,执行yield句子的第一部分逻辑悬挂函数等待再次开始;
d) nret记录的返回值是生成器函数yield后返回到触发器的数据。
2、 以下是老猿编写的模拟存储快递包裹的生成器函数及其呼叫代码。每次执行存储包裹的函数都会挂起。主程序等待确认是否继续循环。如果不继续,则退出。代码如下:
importrandom
defPutPackage():
print(‘PutPackagestart…’)
nRet=123
whileTrue:
ifnRet<1:break
print(‘PutPackage:BeforeYield…’)
nRet=yield’PutPackage’+str(nRet)#返回字符串PutPackage+上次回收yield的返回值
print(‘PutPackage:AfterYield,nRet=’,nRet)
ifnotnRet:continue
defmainf():
print(‘mainfstartcallPutPackage…’)
vPutPackage=PutPackage()#只返回生成器generator对象
bBreak=False
print(‘mainfstartcallnext…’)
nRet=next(vPutPackage)#初始化生成器
print(‘mainfendcallnext,nRet=’,nRet)
whileTrue:
ifbBreak:
try:#为什么要捕获异常?
vPutPackage.send(-1)#触发-1提示生成器函数
exceptStopIteration:pass
break
print(‘mainfloopstartcallsend…’)
nRet=vPutPackage.send(random.randint(10000,9999)#触发生成器函数的随机包装编号
print(‘mainfloopaftercallsend,nRet=’,nRet)
sConfirm=input(“是否准备完成存件提取循环(Y或y是,否则将继续循环):”)
ifsConfirm.strip().upper()==‘Y’:
bBreak=True
print("\n")
mainf()实施结果如下,大家对以前的实施过程进行分析理解:
mainfstartcallPutPackage… mainfstartcallnext… PutPackagestart… PutPackage:BeforeYield… mainfendcallnext,nRet=Package123 mainfloopstartcallsend… PutPackage:AfterYield,nRet=66468 PutPackage:BeforeYield… mainfloopaftercallsend,nRet=PutPackage666668
是否准备完成存件提取循环(Y或y是,否则将继续循环):n
mainfloopstartcallsend… PutPackage:AfterYield,nRet=22204 PutPackage:BeforeYield… mainfloopaftercallsend,nRet=PutPackage2204
是否准备好完成存件取件循环(Y或Y,否则继续循环):y
PutPackage:AfterYield,nRet=-1
为什么要在上述代码中捕获异常?这是因为最后一个send(-1)是从yield的第二部分执行到循环“if nRet<1 : break“语句将终止循环,不会通过yield返回触发器。此时,send执行将出现迭代结束的异常。
3、 生成器函数的其他说明
Python使用生成器支持延迟操作。所谓延迟操作,是指在需要时产生结果,而不是立即产生结果。这有利于节省内存,特别是科学计算生成器;
除next方法外,生成器还可以通过for循环来遍历生成器的内容;
除前面介绍的__外,生成器next__、除了send方法,还有throw、close方法:
a) throw(type[, value[, traceback]]):该方法在生成器暂停位置引起 type 类型异常,并返回生成器函数产生的下一个值。 如果生成器在不产生下一个值的情况下退出,将导致 StopIteration 异常。 如果生成器函数没有捕获传入异常或引起另一个异常,则异常将传播给调用器。该方法可以解决上述案例捕获异常的问题
b) close():在生成器函数暂停的位置引起 GeneratorExit。 如果生成器函数正常退出、关闭或引起 GeneratorExit(因未捕获此异常) 关闭并返回调用器。 假如生成器产生了一个值,关闭就会导致 RuntimeError。 如果生成器引起任何其他异常,它就会传播给调用者。 如果生成器因异常或正常退出 close() 不会做任何事。触发器可以通过调用close直接关闭生成器,而不需要判断生成器函数中send发送的数据,如上述情况。
