深入理解Python的生成器

首先需要弄清关于生成器的一些重要基本概念和规则:
一、生成器由两个单独的部分组成:生成器的函数和生成器的迭代器。生成器的函数是由def语句定义的,其中包含yield语句。生成器的迭代器是这个函数返回的结果,而生成器的函数和生成器的迭代器这两个实体通常被视为一个,统称为生成器;
二、所有包含yield语句的函数都是生成器的函数;
三、yield语句的作用有以下两点:
1、每次返回一个值,有点类似于return语句。
2、冻结执行,程序进入生成器的函数后每次执行到yield语句时就会被暂停,并 随后立即退出函数,等待下一次被唤醒。
四、在生成器的函数被yield语句冻结之后,当程序再次调用next()函数获取生成器的下一个值时,生成器的函数会从上次冻结处被唤醒并继续向下执行;
五、生成器的函数被调用后并不会立即执行,而是返回一个生成器对象。当程序通过next()函数调用生成器或利用for等语句遍历生成器时,生成器的函数才会真正开始执行。
六、“外部程序”可以与生成器动态地交换数据,这要通过生成器的方法send()来实现,例如:a.send(‘Hello’)
其中a是一个生成器对象,实参‘Hello’被传递到生成器的函数中yield表达式,原来的yield语句被视为一个表达式,而这个表达式的值就是’Hello’
七、只有等到生成器被“冻结”之后,外部程序才能使用send()方法向生成器发送数据。如果想获取生成器第一次所生成的值,应该使用next()函数;如果程序非要使用send()方法获取生成器第一次所生成的值,也不能向生成器发送数据,只能为该方法传入None参数

接下来通过一个简单的示例来对程序中生成器的执行流程做一个详细的介绍。

def repeater(value):# {3}
    print('开始执行')# {4}
    while True:# {5}
        new=yield value #{6}{9}{11}{15}
        if new is not None:value=new+'已更新'#{10}{14}
r=repeater(42)# {1}
print(next(r))# {2}{7}
print(next(r))#{8}{12}
print(r.send(None))#{13}{16}

该程序从标号{1}注释的语句开始执行,该语句创建了一个名为r的生成器对象,根据上述概念中的第五条,此时实参42并不会传递到函数里,函数也不会执行。随后程序执行到{2}处,生成器的函数开始执行,42被传递到形参value,程序依次执行至{3}、{4},至此屏幕上会打印出‘开始执行’。程序依次执行至{5}、{6},这里需要注意的是,程序执行到yield value就会立刻冻结,所以此时yield value这个表达式的值并不会赋给new,由于yield value语句发挥作用,此时生成器的迭代器中有了第一个数值42,程序将这个值返回给next®,随后程序立即从函数退出至{7},至此屏幕上会打印出42。随后程序执行至{8},根据上述概念的第四条,程序再次进入生成器的函数,并从上次冻结处被唤醒,执行至{9}的后半段,将yield value这个表达式的值赋给new,由于程序目前还没有利用send()方法向生成器发送数据,所以此时new的值为None,随后程序执行至{10}、{11},再次执行至yield value,生成器的迭代器至此有了第二个值42,随即函数被冻结,程序立即从函数退出至{12},并在屏幕上打印出42。随后程序执行至{13}处,程序再次进入生成器的函数,从上次冻结处被唤醒,这次yield value表达式不再是None,而是‘Hello,world!’,并将其赋给了new变量,程序执行至{14}处,value的值变成了‘Hello,world!已更新’,重复while循环至{15},于是至此生成器的迭代器就有了第三个值‘Hello,world!已更新’,程序将其返回给send方法,执行至{16},并在屏幕上打印出‘Hello,world!已更新’,至此程序执行完毕并退出。可以看出send()方法返回的其实是生成器的迭代器中“生成的下一个值”。下图为程序的执行结果。
程序的执行结果
现在尝试注释掉程序的第7、8行,也就是生成器的函数还没有被冻结,就使用send()方法向其发送数据,看看会发生什么。

结果出现异常,提示信息为
TypeError: can’t send non-None value to a just-started generator
翻译过来就是“不能将非空值发送给一个刚刚启动的生成器”。这也验证了文章开头的第七条规则。究其原因,就是因为如果函数没被冻结就向其发送数据,函数执行到yield语句就冻结不再向下执行了,这意味着发送过来的信息没有赋给变量的机会,也就是这个值“无处存放”,而如果生成器的函数已经执行过yield语句,这时候再使用send()方法,生成器的函数被唤醒后就立刻把发送过来的值赋给变量了。如果程序非要使用send()方法获取生成器第一次所生成的值,也可以将send()方法的参数改成None,这样也不会引发异常,如下图所示。

希望以上对于生成器的执行流程的介绍能帮助大家更好地理解,其中涵盖了关于生成器的最重要也最难理解的知识。关于生成器的其他知识,比如:生成器推导式,生成器的方法throw()、close(),这些内容不是很难理解,很多资料中都有相关介绍,这里也就不再赘述。
最后给大家一个练习题,建议先根据本文内容自行尝试给出下方程序的执行结果,再复制到解释器中执行并验证。

def square_gen(val):
    i = 0
    out_val = None
    while True:
        # 使用yield语句生成值,使用out_val接收send()方法发送的参数值
        out_val = (yield out_val ** 2) if out_val is not None else (yield i ** 2)
        # 如果程序使用send()方法获取下一个值,out_val会获取send()方法的参数
        if out_val is not None : print("====%d" % out_val)
        i += 1

sg = square_gen(5)
# 第一次调用send()方法获取值,只能传入None作为参数
print(sg.send(None))  
print(next(sg)) 
print('--------------')
# 调用send()方法获取生成器的下一个值,参数9会被发送给生成器
print(sg.send(9)) 
# 再次调用next()函数获取生成器的下一个值
print(next(sg)) 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值