Python中的生成器和yield详细介绍


Posted in Python onJanuary 09, 2015

列表推导与生成器表达式

当我们创建了一个列表的时候,就创建了一个可以迭代的对象:

>>> squares=[n*n for n in range(3)]

>>> for i in squares:

 print i

 

0

1

4

这种创建列表的操作很常见,称为列表推导。但是像列表这样的迭代器,比如str、file等,虽然用起来很方便,但有一点,它们是储存在内存中的,如果值很大,会很麻烦。

而生成器表达式不同,它执行的计算与列表包含相同,但会迭代的生成结果。它的语法与列表推导一样,只是要用小括号来代替中括号:

>>> squares=(n*n for n in range(3))

>>> for i in squares:

 print i

 

0

1

4

生成器表达式不会创建序列形式的对象,不会把所有的值都读取到内存中,而是会创建一个通过迭代并按照需求生成值的生成器对象(Generator)。

那么,还有没有其它方法来产生生成器呢?

例子:斐波那契数列

例如有个需求,要生成斐波那契数列的前10位,我们可以这样写:

def fib(n):

    result=[]

    a=1

    b=1

    result.append(a)

    for i in range(n-1):

        a,b=b,a+b

        result.append(a)

    return result

if __name__=='__main__':

    print fib(10)

数字很少时,函数运行良好,但数字很多时,问题就来了,显然生成一个几千几万长度的列表并不是一个很好的主意。

这样,需求就变成了:写一个可以生成可迭代对象的函数,或者说,不要让函数一次返回全部的值,而是一次返回一个值。

这好像与我们的常识相违背,当我们调用一个普通的Python函数时,一般是从函数的第一行代码开始执行,结束于return语句、异常或者函数结束(可以看作隐式的返回None):

def fib(n):

    a=1

    b=1

    for i in range(n-1):

        a,b=b,a+b

        return a

if __name__=='__main__':

    print fib(10)

>>> 

1    #返回第一个值时就卡住了

函数一旦将控制权交还给调用者,就意味着全部结束。函数中做的所有工作以及保存在局部变量中的数据都将丢失。再次调用这个函数时,一切都将从头创建。函数只有一次返回结果的机会,因而必须一次返回所有的结果。通常我们都这么认为的。但是,如果它们并非如此呢?请看神奇的yield:
def fib(n):

    a=1

    yield a

    b=1

    for i in range(n-1):

        a,b=b,a+b

        yield a

if __name__=='__main__':

    for i in fib(10):

        print i

>>> 

1

1

2

3

5

8

13

21

34

生成器Generator

python中生成器的定义很简单,使用了yield关键字的函数就可以称之为生成器,它生成一个值的序列:

def countdown(n):

    while n>0:

        yield n

        n-=1

if __name__=='__main__':

    for i in countdown(10):

        print i

生成器函数返回生成器。要注意的是生成器就是一类特殊的迭代器。作为一个迭代器,生成器必须要定义一些方法,其中一个就是__next__()。如同迭代器一样,我们可以使用next()函数(Python3是__next__() )来获取下一个值:
>>> c=countdown(10)

>>> c.next()

10

>>> c.next()

9

每当生成器被调用的时候,它会返回一个值给调用者。在生成器内部使用yield来完成这个动作。为了记住yield到底干了什么,最简单的方法是把它当作专门给生成器函数用的特殊的return。调用next()时,生成器函数不断的执行语句,直至遇到yield为止,此时生成器函数的”状态”会被冻结,所有的变量的值会被保留下来,下一行要执行的代码的位置也会被记录,直到再次调用next()继续执行yield之后的语句。

next()不能无限执行,当迭代结束时,会抛出StopIteration异常。迭代未结束时,如果你想结束生成器,可以使用close()方法。

>>> c.next()

1

>>> c.next()

StopIteration

>>> c=countdown(10)

>>> c.next()

10

>>> c.close()

>>> c.next()

StopIteration

协程与yield表达式

yield语句还有更给力的功能,作为一个语句出现在赋值运算符的右边,接受一个值,或同时生成一个值并接受一个值。

def recv():

    print 'Ready'

    while True:

        n=yield

        print 'Go %s'%n

>>> c=recv()

>>> c.next()

Ready

>>> c.send(1)

Go 1

>>> c.send(2)

Go 2

以这种方式使用yield语句的函数称为协程。在这个例子中,对于next()的初始调用是必不可少的,这样协程才能执行可通向第一个yield表达式的语句。在这里协程会挂起,等待相关生成器对象send()方法给它发送一个值。传递给send()的值由协程中的yield表达式返回。

协程的运行一般是无限期的,使用方法close()可以显式的关闭它。

如果yield表达式中提供了值,协程可以使用yield语句同时接收和发出返回值。

def split_line():

    print 'ready to split'

    result=None

    while True:

        line=yield result

        result=line.split()

>>> s=split_line()

>>> s.next()

ready to split

>>> s.send('1 2 3')

['1', '2', '3']

>>> s.send('a b c')

['a', 'b', 'c']

注意:理解这个例子中的先后顺序非常重要。首个next()方法让协程执行到yield result,这将返回result的值None。在接下来的send()调用中,接收到的值被放到line中并拆分到result中。send()方法的返回值就是下一条yield语句的值。也就是说,send()方法可以将一个值传递给yield表达式,但是其返回值来自下一个yield表达式,而不是接收send()传递的值的yield表达式。

如果你想用send()方法来开启协程的执行,必须先send一个None值,因为这时候是没有yield语句来接受值的,否则就会抛出异常。

>>> s=split_line()

>>> s.send('1 2 3')

TypeError: can't send non-None value to a just-started generator

>>> s=split_line()

>>> s.send(None)

ready to split

使用生成器与协程

乍看之下,如何使用生成器和协程解决实际问题似乎并不明显。但在解决系统、网络和分布式计算方面的某些问题时,生成器和协程特别有用。实际上,yield已经成为Python最强大的关键字之一。

比如,要建立一个处理文件的管道:

import os,sys

def default_next(func):

    def start(*args,**kwargs):

        f=func(*args,**kwargs)

        f.next()

        return f

    return start

@default_next

def find_files(target):

    topdir=yield

    while True:

        for path,dirname,filelist in os.walk(topdir):

            for filename in filelist:

                target.send(os.path.join(path,filename))
@default_next

def opener(target):

    while True:

        name=yield

        f=open(name)

        target.send(f)

    

@default_next

def catch(target):

    while True:

        f=yield

        for line in f:

            target.send(line)

            

@default_next

def printer():

    while True:

        line=yield

        print line

然后将这些协程连接起来,就可以创建一个数据流处理管道了:
finder=find_files(opener(catch(printer())))

finder.send(toppath)

程序的执行完全由将数据发送到第一个协程find_files()中来驱动,协程管道会永远保持活动状态,直到它显式的调用close()。

总之,生成器的功能非常强大。协程可以用于实现某种形式的并发。在某些类型的应用程序中,可以用一个任务调度器和一些生成器或协程实现协作式用户空间多线程,即greenlet。yield的威力将在协程,协同式多任务处理(cooperative multitasking),以及异步IO中得到真正的体现。

Python 相关文章推荐
Python中的类与对象之描述符详解
Mar 27 Python
Python2.7简单连接与操作MySQL的方法
Apr 27 Python
Python中基础的socket编程实战攻略
Jun 01 Python
Python 抓取微信公众号账号信息的方法
Jun 14 Python
通过PYTHON来实现图像分割详解
Jun 26 Python
浅析Windows 嵌入python解释器的过程
Jul 26 Python
python爬虫 urllib模块反爬虫机制UA详解
Aug 20 Python
Python Process多进程实现过程
Oct 22 Python
Python3运算符常见用法分析
Feb 14 Python
Python 如何批量更新已安装的库
May 26 Python
用python实现学生管理系统
Jul 24 Python
使用python把xmind转换成excel测试用例的实现代码
Oct 12 Python
Python中实现对list做减法操作介绍
Jan 09 #Python
python base64 decode incorrect padding错误解决方法
Jan 08 #Python
Python中字符编码简介、方法及使用建议
Jan 08 #Python
Python实现一个简单的MySQL类
Jan 07 #Python
python实现多线程暴力破解登陆路由器功能代码分享
Jan 04 #Python
Python中对列表排序实例
Jan 04 #Python
Python实现爬取知乎神回复简单爬虫代码分享
Jan 04 #Python
You might like
php 数组二分法查找函数代码
2010/02/16 PHP
简单的PHP多图上传小程序代码
2011/07/17 PHP
ThinkPHP中pathinfo的访问模式、路径访问模式及URL重写总结
2014/08/23 PHP
php简单统计字符串单词数量的方法
2015/06/19 PHP
浅谈php中的访问修饰符private、protected、public的作用范围
2016/11/20 PHP
Iframe自适应高度绝对好使的代码 兼容IE,遨游,火狐
2011/01/27 Javascript
AeroWindow 基于JQuery的弹出窗口插件
2011/06/27 Javascript
Egret引擎开发指南之视觉编程
2014/09/03 Javascript
javascript实现分栏显示小技巧附图
2014/10/13 Javascript
JS实现动态移动层及拖动浮层关闭的方法
2015/04/30 Javascript
js实现适用于素材网站的黑色多级菜单导航条效果
2015/08/24 Javascript
js时间戳转为日期格式的方法
2015/12/28 Javascript
原生 JS Ajax,GET和POST 请求实例代码
2016/06/08 Javascript
微信小程序 数据绑定详解及实例
2016/10/25 Javascript
jQuery利用sort对DOM元素进行排序操作
2016/11/07 Javascript
JS匿名函数类生成方式实例分析
2016/11/26 Javascript
Vue下滚动到页面底部无限加载数据的示例代码
2018/04/22 Javascript
微信小程序实现红包雨功能
2018/07/11 Javascript
详解ES6 export default 和 import语句中的解构赋值
2019/05/28 Javascript
浅谈vue-router路由切换 组件重用挖下的坑
2019/11/01 Javascript
js实现点击生成随机div
2020/01/16 Javascript
numpy给array增加维度np.newaxis的实例
2018/11/01 Python
Python Multiprocessing多进程 使用tqdm显示进度条的实现
2019/08/13 Python
python第三方库学习笔记
2020/02/07 Python
Python 虚拟环境工作原理解析
2020/12/24 Python
找到您丢失的钥匙、钱包和手机:Tile
2017/05/19 全球购物
Wedgwood英国官方网站:英式精致骨瓷餐具、礼品与生活精品,源于1759年
2019/09/02 全球购物
几个Shell Script面试题
2012/08/31 面试题
二手书店创业计划书
2014/01/16 职场文书
物流业务员岗位职责
2015/04/03 职场文书
幼儿园园长安全责任书
2015/05/08 职场文书
2015年副班长工作总结
2015/05/15 职场文书
千手观音观后感
2015/06/03 职场文书
给朋友的赠语
2015/06/23 职场文书
2016年大学迎新晚会工作总结
2015/10/15 职场文书
Mysql基础知识点汇总
2021/05/26 MySQL