Pulpcode

捕获,搅碎,拼接,吞咽

0%

写一些关于yield的东西

用了这么多年的python不可能不知道yield这个利器,迭代器离不开它,协程也离不开它,甚至是那些用yield写好的递归,这里准备写一篇博客,讨论这么长时间,自己对yield的感悟。

本文假设你知道yield,会使用基础的yield,并不会写一些完全入门的介绍。另外,我们把包含yield的函数称之为生成器。

三种yield写法

第一种:

1
2
3
4
def foo():
print "start..."
yield
print "end...."
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> a =foo()
a =foo()
>>> a
a
<generator object foo at 0x1089a5e60>
>>> a.next()
a.next()
start...
>>> a.next()
a.next()
end...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>

第二种:

1
2
3
4
def foo():
print "start..."
yield 5
print "end..."
1
2
3
4
5
6
7
8
9
10
11
12
13
a = foo()
a = foo()
>>> a.next()
a.next()
start...
5
>>> a.next()
a.next()
end...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>

第三种:

1
2
3
4
5
def foo():
print "start..."
n = yield 5
print n
print "end..."
1
2
3
4
5
6
7
8
9
10
11
12
13
14
a = foo()
a = foo()
>>> a.next()
a.next()
start...
5
>>> a.send(6)
a.send(6)
6
end...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>

总结

实际上,你会发现yield就是你暂停的地方,看第一种没有任何参数也能完成暂停,而 m = yield n而言,n就是你调用生成器的返回值(对外),而m就是你send进来的值(对内)。
我一般形象的将yield成为“暂停到表达式中间”,意思是对于yield这样一个表达式,代码执行到yield,会暂停到这里,并将n返回给调用者,然后在下一次“触发”,才会设置m的值,但这前提是你是用send进行触发的,如果是用next进行触发,那么m的值就会是None。

实际用途

很多教学类的博客,都会写一些yield的”纯净码”,但是大多没什么卵用,你会发现完全不能运用到项目中去,这里我就讲几个实际的例子。

xrange的惰性迭代

python中经常会写 for i in range(10) 来做一个循环计数,但是range(10)会直接生成一个列表,那么这将非常浪费空间,因为i只是被用来当做计数,而且随时都有可能跳出。

一个好的写法就是使用xrange,当然在python3中,range默认就是xrange了,其内部就是使用yield,我觉得我可以写一个类似的实现,真实情况是怎样的,我就不得而知了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def xrange(start, stop=None, step=None):
s = 1 # 步长
if not step is None:
s = step
if not stop is None:
while True:
if start < stop:
yield start
start += s
else:
break
else:
stop,start = start,0
while True:
if start < stop:
yield start
start += s
else:
break

tornado的协程

tornado支持异步处理,不过你要写一个费脑仁的回调函数。

1
2
3
4
5
6
client.fetch("http://xxx.xxx.xxx" + \
urllib.urlencode({"q": query, "result_type": "recent", "rpp": 100}),
callback=self.on_response)

def on_response(self, response):
.....

一个不错的写法就是使用协程。

1
2
3
4
5
6
7
8
9
10
@tornado.web.asynchronous
@tornado.gen.coroutine
def get(self):
query = self.get_argument('q')
client = tornado.httpclient.AsyncHTTPClient()
response = yield tornado.gen.Task(client.fetch,
"http://xxx.xxx.xxx" + \
urllib.urlencode({"q": query, "result_type": "recent", "rpp": 100}))
body = json.loads(response.body)
result_count = len(body['results'])

大概的思路就是,你用yield来做一个可重入的函数,装饰器帮你把代码替换成另一种形式,然后底层会在发送请求后,将此io挂起,然后在事件驱动后,再将此io激活。

你通过yield写的可重入函数,要比回调看起来方便许多。