##在这里详述 Py25yieldNote.
## page was renamed from ZoomQuiet
##language:zh
#pragma section-numbers off
<<TableOfContents>>

'''[[PythonZhDoc|中文Python资料收编]]! Py2.5 yield 详说''' -- shhgs
= 发布 =
 * python-cn group 列表 060901 发布
 * [[localshare:doc/Python/_html/Py2.5yield.note/|啄木鸟空间发布 Py2.5yield.note]]

== limodou 之理解 ==
'''2.5版yield之学习心得   Inbox mli_python-cn mli-CZUG'''
{{{		
limodou 	
to python-cn, python-chinese.
	 More options	  9:54 pm (11 hours ago)
}}}
原文发表在我的blog上:  http://blog.donews.com/limodou/archive/2006/09/04/1028747.aspx
这是在看了shhgs关于yield之后,再与他交流后的学习心得。可以算是shhgs文章的一个补充吧。

----

在 shhgs 发布了关于《 Py 2.5 what's new 之 yield》之后,原来我不是特别关注 yield
的用法,因为对于2.3中加入的yield相对来说功能简单,它是作为一个 generator 不可缺少的一条语句,只要包含它的函数即是一个
generator 。但在2.3中,generator 不能重入,不能在运行过程中修改,不能引发异常,你要么是顺序调用,要么就创建一个新的
generator。而且 generator 中的 yield 只是一个语句。但到了 2.5 版之后,情况发生了很在的变化。

在 shhgs 的文章中对于 yield 并没有做太多的描述,也因此让我在理解上产生了许多问题,于是我仔细地研究了 What's new 和
PEP 342 文档,有了一些体会,描述在下面。

这里不说为什么要对 yield 进行修改,只说功能。

 1.  yield 成为了表达式,它不再是语句,但可以放在单独的行上。原文:
{{{
Redefine "yield" to be an expression, rather than a statement. The
current yield statement would become a yield expression whose value is
thrown away.
}}}
可以看到,如果你还是写成语句形式的话,其实还是一个表达式,只是它的值被扔掉了。

那么一个 yield 表达式可以这样写:
{{{
x = yield i
y = x + (yield x)
}}}
那么这种机制到底是如何工作的呢?在2.3版很容易理解,你完全可以把 yield 语句理解为一个 "return" 语句,只不过
"return" 完后,函数并不结束,而是断续运行,直到再次遇到 yield 语句。那么到了 2.5 版不仅仅是一个 "return"
语句那么简单了,让我们看完下面关于 send() 的说明再描述它吧。

 1.  增加了 send(msg) 方法,因此你可以使用它向 generator 发送消息。原文:
{{{
Add a new send() method for generator-iterators, which resumes the
generator and "sends" a value that becomes the result of the current
yield-expression. The send() method returns the next value yielded by
the generator, or raises StopIteration if the generator exits without
yielding another value.
}}}
执行一个 send(msg) 会恢复 generator 的运行,然后发送的值将成为当前 yield 表达式的返回值。然后 send()
会返回下一个被 generator yield 的值,如果没有下一个可以 yield 的值则引发一个异常。

那么可以看过这其实包含了一次运行,从将msg赋给当前被停住的 yield 表达式开始,到下一个 yield
语句结束,然后返回下一个yield语句的参数,然后再挂起,等待下一次的调用。理解起来的确很复杂,不知道你明白了没有。

那么让我们开始想象一下,把 yield 转变为易于理解的东西吧。

我们可以把 yield 想象成下面的伪代码:
{{{
x = yield i ==> put(i); x = wait_and_get()
}}}
可以看到,可以理解为先是一个 put(i),这个 i 就是 yield 表达式后面的参数,如果 yield 没有参数,则表示
None。它表示将 i 放到一个全局缓冲区中,相当于返回了一个值。

`wait_and_get() `可以理解为一个阻塞调用,它等待着外界来唤醒它,并且可以返回一个值。

经过这种转换就容易理解多了。让我们来看一个例子:
{{{
>>> def g():
   print 'step 1'
   x = yield 'hello'
   print 'step 2', 'x=', x
   y = 5 + (yield x)
   print 'step 3', 'y=', y
}}}
很简单,每执行一步都显示一个状态,并且打印出相关变量的值,让我们执行一下看一看。
{{{
>>> f = g()
>>> f.next()
step 1
'hello'
}}}
看见什么了。当我们执行 next() 时,代码执行到 `x = yield 'hello'` 就停住了,并且返回了 yield 后面的
'hello'。如果我们把上面的程序替换成伪代码看一看是什么样子:
{{{
def g():
   print 'step 1'
   put('hello')    #x = yield 'hello'
   x = wait_and get()
   print 'stpe 2', 'x=', x
   put(x)
   y = 5 + wait_and_get()
   print 'step 3', 'y=', y
}}}
可以从伪代码看出,第一次调用 next() 时,先返回一个 'hello', 然后程序挂起在 x = wait_and_get() 上,与我们执行的结果相同。

让我们继续:
{{{
>>> f.send(5)
step 2 x= 5
5
}}}
这次我们使用了 send(5) 而不是 next() 了。要注意 next() 在 2.5 中只算是 `send(None)`
的一种表现方式。正如伪代码演示的,send()一个值,先是激活` wait_and_get()` ,并且通过它返回 send(5)
的参数5,于是 x 的值是 5,然后打印 'step 2',再返回 x 的值5,然后程序挂起在 `y = 5 + wait_and_get()`
上,与运行结果一致。

如果我们继续:
{{{
>>> f.send(2)
step 3 y= 7

Traceback (most recent call last):
 File "<pyshell#13>", line 1, in <module>
   f.send(2)
StopIteration
}}}
可以看到先是激活 wait_and_get(),并且通过它返回 send(2) 的参数 2,因此 y 的值是
7,然后执行下面的打印语句,但因为后面没有下一个 yield 语句了,因此程序无法挂起,于是就抛出异常来。

从上面的伪代码的示例和运行结果的分析,我想你应该对 yield 比较清楚了。还有一些要注意的:
{{{
next()相当于send(None)
yield后面没有参数表示返回为None
}}}
在文档中有几句话很重要:
{{{
Because generator-iterators begin execution at the top of the
generator's function body, there is no yield expression to receive a
value when the generator has just been created. Therefore, calling
send() with a non-None argument is prohibited when the generator
iterator has just started, and a TypeError is raised if this occurs
(presumably due to a logic error of some kind). Thus, before you can
communicate with a coroutine you must first call next() or send(None)
to advance its execution to the first yield expression.
}}}
意思是说,第一次调用时要么使用 next() ,要么使用 send(None) ,不能使用 send() 来发送一个非 None
的值,原因就是第一次没有一个 yield 表达式来接受这个值。如果你转为伪代码就很好理解。以上例来说明,转换后第一句是一个 put()
而不是`wait_and_get()`,因此第一次执行只能返回,而不能接受数据。如果你真的发送了一个非 None 的值,会引发一个
TypeError 的异常,让我们试一试:
{{{
>>> f = g()
>>> f.send(5)

Traceback (most recent call last):
 File "<pyshell#15>", line 1, in <module>
   f.send(5)
TypeError: can't send non-None value to a just-started generator
}}}
看到了吧,果然出错了。

 1.  增加了 `throw()` 方法,可以用来从 generator 内部来引发异常,从而控制 generator 的执行。试验一下:
{{{
>>> f = g()
>>> f.send(None)
step 1
'hello'
>>> f.throw(GeneratorExit)

Traceback (most recent call last):
 File "<pyshell#17>", line 1, in <module>
   f.throw(GeneratorExit)
 File "<pyshell#6>", line 3, in g
   x = yield 'hello'
GeneratorExit
>>> f.send(5)

Traceback (most recent call last):
 File "<pyshell#18>", line 1, in <module>
   f.send(5)
StopIteration
}}}

可以看出,第一次执行后,我执行了一个`f.throw(GeneratorExit)`,于是这个异常被引发。如果再次执行`f.send(5)`,可以看出
generator 已经被停止了。GeneratorExit 是新增加的一个异常类,关于它的说明:
{{{
A new standard exception is defined, GeneratorExit, inheriting from
Exception. A generator should handle this by re-raising it (or just
not catching it) or by raising StopIteration.
}}}
可以看出,增加它的目的就是让 generator 有机会执行一些退出时的清理工作。这一点在 PEP 342 后面的 thumbnail 的例子中用到了。

 1.  增加了 close 方法。它用来关闭一个 generator ,它的伪代码如下(从文档中抄来):
{{{
def close(self):
   try:
       self.throw(GeneratorExit)
   except (GeneratorExit, StopIteration):
       pass
   else:
       raise RuntimeError("generator ignored GeneratorExit")
# Other exceptions are not caught
}}}

因此可以看出,首先向自身引发一个 `GeneratorExit` 异常,如果 generator 引发了 `GeneratorExit` 或
`StopIteration` 异常,则关闭成功。如果 generator 返回了一个值,则引发 `RuntimeError`
异常。如果是其它的异常则不作处理,相当于向上层繁殖,由上层代码来处理。关于它的例子在 PEP 342 中的 thumbnail
的例子中也有描述。

还有其它几点变化,不再做更深入的描述。

关于 PEP 342 中的例子也很值得玩味。简单说一些,其实我也些也不是很懂也就是明白个大概其吧。

文档中一共有4个例子,其实是两个例子构成。

1,2两个例子完成了一个 thunmbnail 的处理。第一个例子 consumer 其实是一个 decorator ,它实现了对一个
generator 的封装,主要就是用来调用一次 next() 。为什么,因为这样调一次下一次就可以使用 send() 一个非 None
的值了,这样后面的代码在使用 generator 可以直接使用 send() 非 None
值来处理了。第二个例子完成对一系列的图片的缩略图的处理。这里每个图片的处理做成了一个 generator,对于图片文件的处理又是一个顶层的
generator ,在这个顶层的 generator 来调用每个图片处理的
generator。同时这个例子还实现了当异常退出时的一种保护工作:处理完正在处理的图片,然后退出。

3,4两个例子完成了一个 echo
服务器的演示。3完成了一个调度器,4是在3的基础上将listen处理和socket联通后的handle处理都转为可调度的 generator
,在调度器中进行调度。同时可以看到 socket 使用的是非阻塞的处理。

通过以上的学习,我深深地感受到 yield 的确很精巧,这一点还是在与 shhgs 语音交流之后才有更深的体会,许多东西可以通过
generator 表现得更优美和精巧,是一个非常值得玩味的东西。以至于 shhgs 感觉到在 2.5 中 yield 比 with
的意义要大。希望大家一同体会。

不过说实在的,yield 的东西的确有些难于理解,要仔细体会才行。

[全文完]

= 原稿 =
{{{
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,以及其它形式的事件驱动或合作多任务编程”。那么我们就从相对简单的合作多任务开始。 



.. _PEP342: http://www.python.org/dev/peps/pep-0342/ 



所谓合作多任务的意思是,一个系统同时有多个任务在运行,而且这些任务都非常的合作,会自愿地 

将系统的控制权转交给其它任务。与多线程相比,合作多任务有两个非常显著的特点。首先是顺序的

决定性。大家都知道多线程环境是非决定性的。各个线程什么时候开始,什么时候挂起都是由线程

调度机制决定的,因此你永远也无法知道某个线程会在什么时候挂起,什么时候重新启动。

而合作多任务从本质上讲还是单线程的程序。只不过我们将每个任务封装成一个单独的函数

(这里就是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,我曾经在邮件列表里写过 `一封信`_ ,

有兴趣的读者可以去看看。



.. _`一封信`: http://groups.google.sm/group/python-cn/browse_thread/thread/1b4903dbf21b4fcf/1c10ce45d41b9246?lnk=raot&hl=it



在异步环境下,你把一堆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,那问题就复杂了。



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



1)  拿到generator之后,第一次只能send一个None,或者调用next。如果你想把接口做得

    友好一点,可以参考 PEP342_ 的consumer函数。这是一个decorator,可以返回一个能直接

    send消息的generator。



2)  connPool和msgQueue是必不可少的。对于读,我们可以不用list。因为不管哪种协议,

    每次循环的时候,每个socket只会读一次。

    但是写必须要用list。因为在有些协议里,比方说IM,

    很可能会出现一次循环里有多个pseudo-thread要往同一个socket里面写东西的情况。

    这时你就必须用list保存数据了。



3)  这一点不是generator的东西。第39行,我们设了sock的timeout,因此47行的时候,

    sock就不会傻等下去了。此外,第56行,select的SOCK_TIMEOUT也很重要。如果你不给

    timeout值,那么select就block了。第一次循环的时候,sock.accept很可能没听到连接,

    因此conns是空的。而select要等至少有一个socket能读写才会退出。于是程序就死了。

    这里你也可以指定timeout为0。这样就变成poll了。



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



         31        while  val:

         32            val = val.strip()

         33            val = yield ( ">>> " + val + '\r\n')

        -->		   if username == 'shhgs' :

        -->    	       sleep(30)



    你会发现,如果shhgs输入了东西,EchoServer就会停上一段时间。从这也能看出,

    coroutine从本质上讲还是单线程的。所以,我们再强调一遍。使用coroutine之前,

    先想好了你的任务是不是适合用coroutine解决。


}}}