Python yield使用详解(一)

刚学到yield这个较为陌生的语法时,一头埋了进去,自认为较为全面的学习到了精髓。结果码了这么多代码好像也没用到过多少次这个关键字,直接扑街。记得工作经验只有一年的时候我出去面了个试,人家问我yield作用是什么,我很自信,一个劲地回答道协程协程,结果好像不太满意,人家一说生成器,奥,恍然大悟,竟然把最基本的语法忘了。所以现在看来,yield是否在协程异步方面有着不可代替的作用,必须出现它呢,那就从篇文章往下看。

1. 生成器

yield语句可以作为生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def countdown(n):
while n > 0:
yield n
n -= 1

# 可以当迭代器来使用它
for x in countdown(10):
print('T-minus', x)

# 可以使用next()来产出值,当生成器函数return(结束)时,报错。
>>> c = countdown(3)
>>> c
<generator object countdown at 0x10064f900>
>>> next(c)
3
>>> next(c)
2
>>> next(c)
1
>>> next(c)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
StopIteration

这篇文章我着重讲yield作为协程的使用方法,作为生成器的话我一笔带过,想要仔细了解迭代器生成器使用,我这里推荐个教程。完全理解Python迭代对象、迭代器、生成器 ,很棒,还有的话就是与生成器密切相关的itertools模块,可以了解下。但是我在讲yield协程之前我再给出一张图来说yield一个有趣的用法。

生成器类似于UNIX管道的作用

这个process会有难以置信的作用,比如实现UNIX中grep的作用。不展开,以后肯定会用到它。

2. 生成器进化为协程

2.1 一个协程例子

重头戏来了。
如果你想更多的使用yield,那么就是协程了。协程就不仅仅是产出值了,而是能消费发送给它的值。
那么这里的例子就用协程实现上面的UNIX的grep作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def grep(pattern):
print("Looking for {}".format(pattern))
while True:
line = yield
if pattern in line:
print('{} : grep success '.format(line))

>>> g=grep('python')
# 还是个生成器
>>> g
<generator object grep at 0x7f17e86f3780>
# 激活协程!只能用一次,也可以用g.send(None)来代替next(g)
>>> next(g)
Looking for python
# 使用.send(...)发送数据,发送的数据会成为生成器函数中yield表达式值,即变量line的值
>>> g.send("Yeah, but no, but yeah, but no")

>>> g.send("A series of tubes")
# 协程,协程,就是互相协作的程序,我发数据过去然后你协助我一下看看grep成功没
>>> g.send("python generators rock!")
python generators rock! : grep success
# 关闭
>>> g.close()

例子讲完了。有几个注意点:

  • 生成器用于生成供迭代的数据
  • 协程是数据的消费者
  • 为了避免脑袋炸裂,不能把两个概念混为一谈
  • 协程与迭代无关
  • 注意,虽然在协程值会使用yield产出值,但这与迭代无关

2.2 发送数据给协程

预激活,到yield处暂停。然后发送item值,协程继续了,协程中item接收到发送的那个值,然后到下一个yield再暂停。

2.3 使用一个装饰器

如果不预激(primer),那么协程没什么用,调用g.send(x)之前。记住一定要调用next(g)。为了简化协程用法,有时会使用一个预激装饰器,如下。

1
2
3
4
5
6
7
8
9
10
def coroutine(func):
def primer(*args,**kwargs):
cr = func(*args,**kwargs)
next(cr)
return cr
return primer

@coroutine
def grep(pattern):
...

2.4 关闭一个协程

  • 一个协程有可能永远运行下去
  • 可以 .close()让它停下来
    例子中已经体现,不展开。

2.5 捕捉close()

1
2
3
4
5
6
7
8
9
def grep(pattern):
print("Looking for {}".format(pattern))
try:
while True:
line = yield
if pattern in line:
print(line)
except GeneratorExit:
print("Going away. Goodbye")

捕捉到.close()方法,然后会打印"Going away. Goodbye"

2.6 抛出异常

1
2
3
4
5
6
7
8
9
10
11
>>> g = grep("python")
>>> next(g) # Prime it
Looking for python
>>> g.send("python generators rock!")
python generators rock! : grep success
>>> g.throw(RuntimeError,"You're hosed")
Traceback (most recent call last):
.....
.....
RuntimeError: You're hosed
>>>

说明:

  • 在协程内部能抛出一个异常
  • 异常发生于yield表达式
  • 不慌,我们可以平常的方法处理它

2.7 生成器返回数值

鉴于上面的例子是一直run下去的,所以稍加修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def grep(pattern):
print("Looking for {}".format(pattern))
while True:
line = yield
# 当发送的数据为None时,跳出while循环
if line is None:
break
else:
if pattern in line:
print('{} : grep success '.format(line))
return 'End'

>>> ..... 省略
>>> g.send(None)
Traceback (most recent call last):
...
...
StopIteration: End

# 这里可以用try捕捉异常,异常对象的value属性保存着返回的值
try:
g.send(None)
except StopIteration as exc:
result = exc.value

>>> result
#End

图解如下

说明:

  • 通过捕捉异常获取返回值
  • 只支持python3

3. 总结

  • yield的基本用法已经差不多了,有两个方面:生成器与协程(理解协程的关键在于明白它在何处暂停发送出的数据传到了哪个变量)
  • yield的另一方面的应用是上下文管理器下一节讲
  • yield from我这里暂时不讲,留到后面。yield from会在内部自动捕获StopIteration异常等