浅谈Python生成器generator

普通函数在return执行后便会完全退出,其内部的临时变量也会被销毁,然而在Python中,生成器函数允许自身“暂时”退出,在下一次调用它自己的时候,会从上次退出之处接着执行相应语句。而要实现这一功能,需要用到yield关键字。一旦函数中使用了yield,该函数则自动变为生成器函数。

生成器函数是一类特殊的迭代器,实际上它与普通函数区别并不大,以下将介绍生成器的特殊使用及其方法:

生成器的使用

yield

yield相当于return,后面可接多种类型的返回值。生成器函数在执行到yield后会暂时退出,回到调用它的函数,然而生成器函数内的临时变量、执行程度都会保留,下次调用该函数时,会从yield的下一句开始继续执行,直到生成器函数的最后。

生成器函数可以有多个yield,也可以将yield写入循环反复执行。

举个例子:(右侧序号为程序执行顺序)

输出为:

next

举个例子:(输出斐波那契数列的前100项)

def fo():
    a, b = 0, 1
    while True:
        yield b
        a, b = b, a+b

gen = fo()
for i in range(100):
    print(next(gen))

生成器函数通过yield返回,但函数本身的返回值并非yield后所接的值,而是其生成器,因此不可直接print(gen)或者print(fo())。

要得到yield的结果,需要用到next()来进行迭代,而使用next,则先要通过gen = fo()这一步,这一步是使用生成器函数fo()生成了一个生成器gen(相当于实例化一个对象),因为生成器自带有next方法,可以用next()进行迭代(除了next(gen),也可以写成gen.__next__())。

实际上,生成器函数fo()也可以调用next方法,但它本身并无生成器迭代的功能,直接print(next(fo()))的结果就是,生成器函数退化为普通函数,yield成为了return,100次循环就会输出100个1。

除了直接使用next方法,还可以通过for循环调用,这是因为for循环内含next方法。此外,如果生成器函数所有的yield执行完毕,或者执行到return语句时,再使用next会报错:StopIteration,而for循环会自动忽略这一错误。

输出100以内的斐波那契数列:(与上面的功能略有不同)

def fo(max):
    a , b = 0, 1
    while b < max:
        yield b
        a, b = b, a+b

for i in fo(100):
    print(i)

如果要捕获生成器函数的return值,或者防止StopIteration错误中断程序,我们可以:

def fo():
    yield 1
    yield 2
    return 3

gen = fo()
while True:
    try:
        print(next(gen))
    except StopIteration as e:
        print(e)
        break

若没有return或return后不带值,e的值为空。

Send

通过send函数,可以实现往生成器函数中传值的功能:

def fo():
    while True:
        t = yield '在这停顿'
        print(t)

gen = fo()
print(next(gen))
print(next(gen))
print(gen.send(5))

输出为:

第一次执行输出t,发现结果为None,这是因为yield后的值返回出后被next方法捕获,也就没有任何值被赋给t,之后在gen.send(5)处,send方法本身也有next的功能,回到上次中断处,将传入的5赋值给了t(因此传入的值可以与yield值的类型不同)。可以理解为,要给生成器函数内某变量传入值,应该在函数执行的相应阶段通过send传入,接收方必须使用t = yield ...的形式接收。

close

关闭生成器,之后若再用next或其他方法调用,无论有没有yield都会直接抛出StopIteration错误。

def fo():
    yield 1
    yield 2

gen = fo()
print(next(gen))
gen.close()
print(next(gen))

最后执行next(gen),虽然之前才执行到yield 1,但因为close了生成器,这里就无法输出2,直接报错:

 

生成器作用

模拟并发

在函数A执行时,可用yield将其挂起,再运行函数B,然后再用next唤醒A继续执行。用这种方法可以非常简便地模拟并发,不必引入复杂繁琐的多线程代码。(然而,这仅仅是模拟,要实现真正的多线程还需走正道……)

节省空间

生成器保存的是算法,只有在next()调用时,才会开始执行,直到下一个yield。

假如说有某个函数,现在传入一个极大的数据规模,大到现有内存完全不够用,这个时候可以将这个函数改写成生成器函数,分批处理,分批返回结果,则可解决上述问题。

例如在文件读写时,为了防止f.read()时无法预知的内存使用,可设定每次读入的数据量,分批读入,代码也比较简单,易于实现。

def read_file(path):
    size = 1024
    with open(path, 'rb') as f:
        while True:
            block = f.read(size)
            if block:
                yield block
            else:
                return

简化代码

在实现上述功能的同时,我们可以发现代码都较为简洁,这同样也是生成器的一大特点。这里再举一个例子:

fo = (x*x for x in range(100))
for i in fo:
    print(i)

得到前100个平方数,写起来极为简单。(当然也可以用列表实现,把fo后的小括号换成方括号就行)

 

小结

生成器使用简便,功能强大,前文已经介绍过了。这里讲讲我的一些想法。对于节省空间这一点,我认为生成器其实是一种”妥协“,以时间换空间,同样是生成斐波那契数列,生成器固然方便、迅速,但无法存储数列的每一项(否则就和列表一样了),在小数据量的情况下对于多次查询任务,效率会较低(每次都要从头算起,除非将查询排序)。当然,在超大数据量下(爆内存那种)生成器还是有绝对优势的。

PS:若是极为简单的任务,例如上面输出前100个平方数的,其实直接使用for循环会更快,而且更简单:

for i in range(100):
    print(i)

 

参考资料

http://www.runoob.com/w3cnote/python-yield-used-analysis.html

http://3g.163.com/dy/article/DCVHKB5E0516U4OP.html

http://python.jobbole.com/87613/

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值