您的位置:首页 > 编程语言 > Python开发

python之yield理解

2013-01-14 15:36 337 查看


生成器的设计动机(PEP255)

2.3版本加入yield关键字

编程场景中有一类Producor Function,随着调用会生产一系列值。例如,在用于源码解析的tokenize模块中,分析源码时不断通过token返回片段,解决这一问题通常有以下几个方法:

在全局变量中维护一个状态机用于记录目前进行到哪里,希望下一步获得什么,通过回调方法与外部互动;缺点:状态机维护非常复杂,难以理解。

将目标数据全部解析,将跳转逻辑写入控制流,以备调用;缺点:浪费空间。

使用迭代器,当调用next的时候进行下一步;缺点:需要手工维护状态,而且不灵活(看C++ STL的实现可以想象,只适合简单逻辑)。

利用生产者/消费者模型,将生产与调用过程分开;缺点:需要多线程支持,效率不高。

Stackless实现,与4想法类似,使用Stackless提高效率;缺点:需要重新设计Python核心。

可以看到上面的方法都有些许不足,于是,决定采用生成器来解决这个问题,目标是,方法可以保持状态,外部再次调用时可以重新启动,进行“生产(yield)”,不断返回。

yield保留字与return语句的返回值和执行原理都不相同,yield生成值并不会中止程序的执行,返回值后程序继续往后执行,return返回值后,程序将中止执行

def fib(max):  
    a, b = 0, 1            
    while a < max:  
        yield a            
        a, b = b, a + b
生成器(generator)的作用是一次产生一个数据项,并把数据项输出,generator函数可以用在for循环中遍历.generator函数的定义和普通函数的定义没什么区别,只要在函数体内使用yield生成数据项即可.generator函数可以被for循环遍历,并且可以通过next()方法获得yield生成的数据项。

只是粗略的知道yield可以用来为一个函数返回值塞数据,比如下面的例子:

def addlist(alist):

for i in alist:

yield i + 1
取出alist的每一项,然后把i + 1塞进去。然后通过调用取出每一项:

alist = [1, 2, 3, 4]

for x in addlist(alist):

print x,
这的确是yield应用的一个例子,但是,看过limodou的文章《2.5版yield之学习心得》,并自己反复体验后,对yield有了一个全新的理解。


1. 包含yield的函数

假如你看到某个函数包含了yield,这意味着这个函数已经是一个Generator,它的执行会和其他普通的函数有很多不同。比如下面的简单的函数:

def h():

print 'To
be brave'

yield 5

h()
可以看到,调用h()之后,print 语句并没有执行!这就是yield,那么,如何让print 语句执行呢?这就是后面要讨论的问题,通过后面的讨论和学习,就会明白yield的工作原理了。


2. yield是一个表达式

Python2.5以前,yield是一个语句,但现在2.5中,yield是一个表达式(Expression),比如:

m = yield 5
表达式(yield 5)的返回值将赋值给m,所以,认为 m = 5 是错误的。那么如何获取(yield 5)的返回值呢?需要用到后面要介绍的send(msg)方法。


3. 透过next()语句看原理

现在,我们来揭晓yield的工作原理。我们知道,我们上面的h()被调用后并没有执行,因为它有yield表达式,因此,我们通过next()语句让它执行。next()语句将恢复Generator执行,并直到下一个yield表达式处。比如:

def h():

print 'Wen
Chuan'

yield 5

print 'Fighting!'

c = h()

c.next()
c.next()调用后,h()开始执行,直到遇到yield 5,因此输出结果:

Wen Chuan

当我们再次调用c.next()时,会继续执行,直到找到下一个yield表达式。由于后面没有yield了,因此会拋出异常:

Wen Chuan

Fighting!

Traceback (most recent call last):

File "/home/evergreen/Codes/yidld.py",
line 11, in <module>

c.next()

StopIteration


4. send(msg) 与 next()

了解了next()如何让包含yield的函数执行后,我们再来看另外一个非常重要的函数send(msg)。其实next()和send()在一定意义上作用是相似的,区别是send()可以传递yield表达式的值进去,而next()不能传递特定的值,只能传递None进去。因此,我们可以看做

c.next() 和 c.send(None) 作用是一样的。

来看这个例子:

def h():

print 'Wen
Chuan',

m = yield 5 # Fighting!

print m

d = yield 12

print 'We
are together!'

c = h()

c.next() #相当于c.send(None)

c.send('Fighting!') #(yield
5)表达式被赋予了'Fighting!'
输出的结果为:

Wen Chuan Fighting!

需要提醒的是,第一次调用时,请使用next()语句或是send(None),不能使用send发送一个非None的值,否则会出错的,因为没有yield语句来接收这个值。


5. send(msg) 与 next()的返回值

send(msg) 和 next()是有返回值的,它们的返回值很特殊,返回的是下一个yield表达式的参数。比如yield 5,则返回 5 。到这里,是不是明白了一些什么东西?本文第一个例子中,通过for i in alist
遍历 Generator,其实是每次都调用了alist.Next(),而每次alist.Next()的返回值正是yield的参数,即我们开始认为被压进去的东东。我们再延续上面的例子:

def h():

print 'Wen
Chuan',

m = yield 5 # Fighting!

print m

d = yield 12

print 'We
are together!'

c = h()

m = c.next() #m
获取了yield 5 的参数值 5

d = c.send('Fighting!') #d
获取了yield 12 的参数值12

print 'We
will never forget the date', m, '.',
d
输出结果:

Wen Chuan Fighting!

We will never forget the date 5 . 12


6. throw() 与 close()中断 Generator

中断Generator是一个非常灵活的技巧,可以通过throw抛出一个GeneratorExit异常来终止Generator。Close()方法作用是一样的,其实内部它是调用了throw(GeneratorExit)的。我们看:

def close(self):

try:

self.throw(GeneratorExit)

except (GeneratorExit, StopIteration):

pass

else:

raise RuntimeError("generator
ignored GeneratorExit")

# Other exceptions are not caught
因此,当我们调用了close()方法后,再调用next()或是send(msg)的话会抛出一个异常:

Traceback (most recent call last):

File "/home/evergreen/Codes/yidld.py",
line 14, in <module>

d = c.send('Fighting!') #d
获取了yield 12 的参数值12

StopIteration

注:以上观点属于本人的个人理解,如有偏差请批评指正。谢谢!

转自:http://www.jb51.net/article/15717.htm

在 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() 的说明再描述它吧。

2. 增加了 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

看到了吧,果然出错了。

3. 增加了 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 的例子中用到了。

4. 增加了 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 的东西的确有些难于理解,要仔细体会才行。

转自:http://blog.donews.com/limodou/archive/2006/09/04/1028747.aspx

n皇后实现:

def conflict(state, nextY):
	nextX = len(state)
	for i in range(nextX):
		if abs(state[i]-nextY) in (0,nextX-i):
			return True
	return False
def queens(num=8, state=()):
	if len(state) == num-1:
		for pos in range(num):
			if not conflict(state, pos):
				yield pos
	else:
		for pos in range(num):
			if not conflict(state, pos):
				for r in queens(num, state+(pos,)):
					yield (pos,)+(r,)


def queens(n=8, state=()):
	for pos in range(n):
		if not conflict(state, pos):
			if len(state) == n-1:
				yield(pos,)
			else:
				for r in queens(n, state+(pos,)):
					yield (pos,)+(r,)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: