免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
12下一页
最近访问板块 发新帖
查看: 12967 | 回复: 11
打印 上一主题 下一主题

Py 2.5 what's new 之 yield(灰衣人) [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2006-09-01 11:08 |只看该作者 |倒序浏览
作者:灰衣人
Py 2.5 what's new 之 yield
Date: 2006-8-31
Author: shhgs
Copyright: 为了表达本人对CSDN论坛“脚本语言(Perl/Python)”专区的强烈不满,特此宣布,本文档不允许任何人发布或者链接到CSDN论坛的“脚本语言Perl/Python”专区。除此之外,任何人均可以阅读,分发本文档的电子版,或者本文档的链接。此外,任何人均可以将本文档张贴到除CSDN论坛“脚本语言Perl/Python”专区之外的其它 BBS。任何人均可以打印本文档,以供自己或他人使用,但是不得以任何名义向任何人收取任何费用。上述名义包括,但不限于,纸张费,打印费,耗材费等等。分发、张贴本文档的时候,必须保留这段版权申明。如果有人要出版本文档,必须事先获得本人的同意。

Py 2.5 对yield做了本质性的增强,使得Py有了自己的first class的coroutine。

我们先来看看传统的yield。Py 2.3加入的yield使得Python实现了first class的generator。 generator是enumerator/iterator的自然延伸,其区别在于,iterator/enumerator遍历的是一个既有的有限集合,而generator则是依次生成集合的各个元素,并且这个集合还可以是无限的。从算法上讲,generator同递归一样,源出数学归纳法,但是与递归相比,一是其代码更为清晰,二是它没有嵌套层数的限制。

但是你提供工具想让别人干什么和别人会怎么去用这个工具,从根本上讲是两码事。generator 问世之初就有人敏感地指出,这是一个semi coroutine。所谓的coroutine是指,一种有多个 entry point和suspend point的routine。Py 2.3的yield实现了多个entry/suspend point,但是由于其无法在generator每次重新启动的时候往里面传新的数据,因此只能被称作semi coroutine。

当然也不是全然没有办法。但是总的来说,要想往里面传新的数据,你就得动用一些技巧。本文的主旨不在于向诸位介绍这些技巧,这里我们关心的是,为什么那些大牛们要挖空心思去改造generator,他们想要干什么,以及怎么干。

Py 2.5 yield 的语法
讲了半天往generator里面传数据,那么怎么个传法呢?

Py 2.5的generator有了一个新的send方法,我们就是用这个send往里面传数据。:

gen.send(message)

那么generator又是怎样接收数据的呢?这里,Py 2.5对yield的语法做了改造。现在yield已经不是一个语句了,而是一个表达式。因此当你:

val = yield i

传给generator的值就被赋予val了,而generator还是像以前那样生成i。

现在:

gen.next()

成了:

gen.send(None)

的简写,而:

yield i

则表示generator会忽略传进来的值。

yield的语法就这么简单,如果读者还有什么疑问的话,可以参看Python Manual里面的what's new。

yield的用途
1. 合作多任务
PEP342 提到的coroutine的用途包括“模拟,游戏,异步I/O,以及其它形式的事件驱动或合作多任务编程”。那么我们就从相对简单的合作多任务开始。

所谓合作多任务的意思是,一个系统同时有多个任务在运行,而且这些任务都非常的合作,会自愿地将系统的控制权转交给其它任务。与多线程相比,合作多任务有两个非常显著的特点。首先是顺序的决定性。大家都知道多线程环境是非决定性的。各个线程什么时候开始,什么时候挂起都是由线程调度机制决定的,因此你永远也无法知道某个线程会在什么时候挂起,什么时候重新启动。而合作多任务从本质上讲还是单线程的程序。只不过我们将每个任务封装成一个单独的函数 (这里就是generator),然后通过调度程序按照一定的算法轮流调用这种函数,从而推进任务的进展。

讲到这里,大家应该对“合作”有一点体会了。这里,每个任务都必须合作,也就是说必须能在较短的时间里将系统的控制权转交出去。如果某个任务进入了死循环,那么整个系统也就死了。

下面我们就来举一个用generator实现合作多任务的例子。假设这是一盘棋,电脑引擎和用户界面程序分别做成了generator。:

player = GetUserInput(...)
engine = Engine(...)

def game(red, black) :
    ...
    move = red.next()
    while move != Move.Resign :
        if turn == black :
            turn = red
        else :
            turn = black
        game_state.update(move)
        move = yield turn.send(move)
    game_state.update(move)

这里能很清楚地看出generator所实现的合作多任务的单线程本质。因此如果我们的象棋引擎耍赖的话,:

def Engine() :
    ...
    if game.LoseInevitable :
    while 1 :
        sleep(1000)
    yield Move.Resign

那么你的程序就死了。

这是合作多任务的先天缺陷,因此在设计的时候你就得想好了,这个任务是不是适合用合作多任务来解决。

2. 异步I/O
coroutine的另一个用途是异步I/O。关于异步I/O,我曾经在邮件列表里写过 一封信 ,有兴趣的读者可以去看看。

在异步环境下,你把一堆socket交给监听器。监听器则负责告诉你socket是不是可读可写。监听器只能帮你把数据读出来,至于读出来的东西是不是合法,该怎么用,它就无能为力了。因此你得写一大堆回调函数,让监听器帮你把信息分发到回调函数里。

这个任务可不容易。因为监听器是根据收到的信息来判断调用哪个回调函数的,但是函数却不一定知道该怎么处理这个信息。比方说,监听器听到用户输入了一个PASS命令,于是调用do_PASS。但是这个口令是谁的,或者用户先前有没有使用USER命令,监听器都不知道。既然监听器不知道,do_PASS也就无从获知,因此回调函数里面还有一大堆麻烦事等着。

有了coroutine之后,我们可以将每个会话封装成一个generator。当监听器听到数据的时候,可以用send方法,把信息传给coroutine,让coroutine继续运行,等yield完值之后再睡。coroutine的这种工作方式与线程很相似,因此也被称作pseudo-thread。

下面我们举一个完整的例子。程序清单如下:

  1    #!/usr/local/bin/python2.5
  2
  3    import socket, select, collections
  4
  5    SOCK_TIMEOUT = 0.1
  6    BUFSIZ = 8192
  7    PORT   = 10000
  8
  9    def get_auth_config() :
10        return {'shhgs': 'hello', 'limodou': 'world'}
11
12    def task() :
13        authdb = get_auth_config()
14
15        username = yield 'Greetings from EchoServer on %s\nUserName Please: \r\n' % socket.gethostname()
16
17        username = username.strip()
18        if username not in authdb :
19            yield '\nInvalid user. Byebye\r\n'
20            return
21        else :
22            password = yield '\nYour Password Please:\r\n'
23
24        password = password.strip()
25        if authdb[username] == password :
26            val = yield '\nMay you enjoy the EchoServer.\r\n'
27        else :
28            yield '\nWrong Password\r\n'
29            return
30
31        while  val:
32            val = val.strip()
33            val = yield ( ">>> " + val + '\r\n')
34
35    def main(proto) :
36        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
37        sock.bind(('' , PORT))
38        sock.listen(5)
39        sock.settimeout(SOCK_TIMEOUT)
40
41        connPool = {}        # 这两个变量相当主要,主控程序要通过connPool选择pseudo-thread
42        msgQueue = {}        # 而msgQueue则是存储传入generator的消息队列的
43
44        try :
45            while 1 :
46                try :
47                    conn, addr = sock.accept()
48                    connPool[conn] = proto()
49                    greetings = connPool[conn].next()    # 注意,第一次调用generator的send时,只能传None。或者像这样,调用next
50                    conn.sendall(greetings)
51                except socket.timeout :
52                    pass
53
54                conns = connPool.keys()
55                try :
56                    i,o,e = select.select(conns, conns, (), SOCK_TIMEOUT )
57                except :
58                    i = o = e = []
59
60                for conn in i :
61                    try :
62                        data = conn.recv(BUFSIZ)
63                        if data :
64                            response = connPool[conn].send(data)
65                            if conn in msgQueue :
66                                msgQueue[conn].append(response)   # msgQueue的值必须是list
67                            else :
68                                msgQueue[conn] = [ response, ]
69                    except socket.error :
70                        try :
71                            connPool.pop(conn)
72                            msgQueue.pop(conn)
73                        except :
74                            pass
75                        conn.close()
76
77                for conn in o :
78                    try :
79                        if conn in msgQueue :
80                            msgs = msgQueue.pop(conn)
81                            for response in msgs :
82                                conn.sendall(response)
83                                if response in ('\nInvalid user. Byebye\r\n', '\nWrong Password\r\n') : # 终于知道正规的协议为什么都是用错误号的了。
84                                    connPool.pop(conn)
85                                    conn.close()
86                    except socket.error :
87                        try :
88                            connPool.pop(conn)
89                            msgQueue.pop(conn)
90                        except :
91                            pass
92                        conn.close()
93
94        except :
95            sock.close()
96
97    if __name__ == "__main__" :
98    #    t = task()
99    #    input = raw_input(t.next())
100    #    while input :
101    #        resp = t.send(input)
102    #        input = raw_input(resp)
103        main(task)

task就是一个pseudo-thread,其调试部分在最后,就是被注释掉的那几行。如果把raw_input代进去,这就是一个非常简单的程序,相信初学者也应该能写。但是如果你要求用callback,那问题就复杂了。

主控程序虽然比较长,但也很简单。这里主要提几个地方。

拿到generator之后,第一次只能send一个None,或者调用next。如果你想把接口做得友好一点,可以参考 PEP342 的consumer函数。这是一个decorator,可以返回一个能直接 send消息的generator。

connPool和msgQueue是必不可少的。对于读,我们可以不用list。因为不管哪种协议,每次循环的时候,每个socket只会读一次。但是写必须要用list。因为在有些协议里,比方说IM,很可能会出现一次循环里有多个pseudo-thread要往同一个socket里面写东西的情况。这时你就必须用list保存数据了。

这一点不是generator的东西。第39行,我们设了sock的timeout,因此47行的时候, sock就不会傻等下去了。此外,第56行,select的SOCK_TIMEOUT也很重要。如果你不给 timeout值,那么select就block了。第一次循环的时候,sock.accept很可能没听到连接,因此conns是空的。而select要等至少有一个socket能读写才会退出。于是程序就死了。这里你也可以指定timeout为0。这样就变成poll了。

coroutine本质上还是单线程。读者可以这样修改程序:

31        while  val:
32            val = val.strip()
33            val = yield ( ">>> " + val + '\r\n')
-->                if username == 'shhgs' :
-->            sleep(30)

你会发现,如果shhgs输入了东西,EchoServer就会停上一段时间。从这也能看出, coroutine从本质上讲还是单线程的。所以,我们再强调一遍。使用coroutine之前,先想好了你的任务是不是适合用coroutine解决。

论坛徽章:
0
2 [报告]
发表于 2006-09-01 11:15 |只看该作者

论坛徽章:
0
3 [报告]
发表于 2006-09-01 11:31 |只看该作者
谢谢Shhgs! 以后有问题多向你请教

论坛徽章:
4
CU大牛徽章
日期:2013-03-13 15:29:07CU大牛徽章
日期:2013-03-13 15:29:49CU大牛徽章
日期:2013-03-13 15:30:192015亚冠之广州恒大
日期:2015-07-22 17:20:15
4 [报告]
发表于 2006-09-11 13:38 |只看该作者
顶起来 嘿嘿

论坛徽章:
0
5 [报告]
发表于 2006-09-26 08:34 |只看该作者
谢谢,从中收益了

论坛徽章:
0
6 [报告]
发表于 2007-01-07 22:19 |只看该作者
为什么对CSDN这些反感??我觉得csdn的python板不错呀

论坛徽章:
0
7 [报告]
发表于 2007-01-25 14:45 |只看该作者

请修正一下帖子权限设置,因为匿名不能查看,

既然允许转载,禁止匿名查看应该不是你的本意吧。

论坛徽章:
0
8 [报告]
发表于 2007-08-17 19:20 |只看该作者
确实是好东西,本人正在学习,但结合c/c++和python做一个大工程感觉自己还是太欠缺了!
顶!!!

论坛徽章:
0
9 [报告]
发表于 2007-09-07 20:39 |只看该作者
NN留个QQ号呀

论坛徽章:
0
10 [报告]
发表于 2007-10-24 11:54 |只看该作者
虽然没看完,但是说声谢谢!希望将来python3.0出来后作者也能继续为大家写个指南。
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP