前言

yield这个关键字很早的时候就了解过,但一直都只了解其基本使用,即转变函数为生成器的使用,节省大型迭代时的内存空间,但其实yield在python的很多特性中都起着重要的作用

这篇文章就详细展开一下yield关键字

需要了解的几个词

  • 容器(container):python中容器指一个用来存储多个元素的数据结构,常见的list,tuple,dict,set,string都是容器
  • 可迭代对象(iterable):在python中能被迭代获取的对象,iterable的对象一定实现了__iter__()方法
  • 迭代器(iterator):迭代器实现了__iter__()__next__()方法,是一个带状态的对象,迭代器内部持有一个状态,该状态用于记录**当前迭代所在位置,**以便于下次迭代的时候获取正确的元素,迭代器可以通过next()方法来迭代获取下一个值
  • 生成器(generator):生成器是一种特殊的迭代器,特殊在我们可以通过send()方法向生成器中传入数据,而迭代器只能将数据输出
  • 协程(coroutine):与线程很相似,不同之处在于多协程是同一个线程来执行的,这样就省去了线程切换的时间,而且不需要多线程的锁机制了,执行效率高很多

可迭代对象,迭代器与生成器的关系

简单来说可以用以下的韦恩图表示:

ii

从设计角度讲,容器是我们最常见最常用的数据结构,它们都是可迭代对象,而另一方面我们也可以在自定义类中实现__iter__()方法将该类的对象变成可迭代的

但这样的迭代有一个缺点,当一次迭代的内容过多时,会占用大量内存;这时迭代器就出现了,其实现了__next__()方法,在每个单次迭代中记录位置,每次返回一个值来进行完整的迭代,实现一种惰性的获取数据的方式;这样就不需要一次性把所有内容加载到内存,而是在需要的时候返回单个结果

生成器是一种特殊的迭代器,可以通过生成器函数和生成器表达式来创建生成器,其自带了__iter__()__next__()方法,通过创建生成器来创建迭代器可以让我们更专注于业务逻辑的实现;此外其还实现了send()方法,可以往生成器函数中传值,赋给yield关键字的左值

生成器中的yield

在一个函数中使用yield关键字,这个函数就变成了生成器函数,看一个经典的输出斐波那契数列实现:

1
2
3
4
5
6
7
8
9
10
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'

for num in fib(1000):
print(num)

非常优雅地就可以实现斐波那契数列的输出,并且在进行for循环时每次只占用一个数的内存

但使用for循环进行迭代我们无法获取到生成器函数的返回值(生成器结束迭代时会抛出StopIteration异常,但这个异常被for循环捕获并pass了);想要获取返回值我们需要抛弃for循环,自己来捕获异常:

1
2
3
4
5
6
7
8
g = fib(6)
while True:
try:
x = next(g)
print('g:', x)
except StopIteration as e:
print('Generator return value:', e.value)
break

协程中的yield

Python对协程的支持是通过generator实现的,在一般的generator使用中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值;但在生成器中我们还可以通过send向生成器函数中传值;到这里我们就可以拿出经典的生产者-消费者模型来看看了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def consumer():
res = ''
while True:
product = yield res
if not product:
return
print('[CONSUMER] Consuming %s...' % product)
res = '200 OK'

def produce(c):
c.send(None)
product = 0
while product < 5:
product += 1
print('[PRODUCER] Producing %s...' % product)
r = c.send(product)
print('[PRODUCER] Consumer return: %s' % r)
c.close()

c = consumer()
produce(c)

注意到consumer函数是一个generator,把一个consumer传入produce后:

  1. 首先调用c.send(None)启动生成器;
  2. 然后,一旦生产了东西,通过c.send(n)切换到consumer执行;
  3. consumer通过yield拿到消息,处理,又通过yield把结果传回;
  4. produce拿到consumer处理的结果,继续生产下一条消息;
  5. produce决定不生产了,通过c.close()关闭consumer,整个过程结束。

整个流程无锁,由一个线程执行,produceconsumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务

如果没有协程,我们在写一个并发业务时会遇到以下问题:

  • 使用最常规的同步编程要实现异步并发效果并不理想,或者难度极高
  • 由于GIL锁的存在,多线程的运行需要频繁的加锁解锁,切换线程,这极大地降低了并发性能

而有了协程,我们就可以非常优雅高性能地实现一些高IO的并发任务了

yield from

yield fromPython 3.3中才出现的语法,所以这个特性在Python 2中是没有的,yield from语法可以让我们方便地调用另一个generator

yield from 后面需要加的是可迭代对象,它可以是普通的可迭代对象,也可以是迭代器或生成器(不记得关系了的可以往上翻一翻)

应用一:拼接可迭代对象

我们可以用一个使用yield和一个使用yield from的例子来对比看

使用yield

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 字符串
astr = 'ABC'
# 列表
alist = [1,2,3]
# 字典
adict = {"name":"wangbm","age":18}
# 生成器(生成器表达式)
agen = (i for i in range(4,8))

def gen(*args, **kw):
for item in args:
for i in item:
yield i

new_list = gen(astr, alist, adict, agen)
print(list(new_list))
# ['A', 'B', 'C', 1, 2, 3, 'name', 'age', 4, 5, 6, 7]

使用yield from

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 字符串
astr = 'ABC'
# 列表
alist = [1,2,3]
# 字典
adict = {"name":"wangbm","age":18}
# 生成器
agen = (i for i in range(4,8))

def gen(*args, **kw):
for item in args:
yield from item

new_list = gen(astr, alist, adict, agen)
print(list(new_list))
# ['A', 'B', 'C', 1, 2, 3, 'name', 'age', 4, 5, 6, 7]

由上面两种方式的对比,可以看出,yield from后面加上可迭代对象,他可以把可迭代对象里的每个元素一个一个的yield出来,对比yield来说代码更加简洁,结构更加清晰

应用二:生成器的嵌套

上面的只是yield from很简单的一个应用,它真正的作用并不在此;当 yield from 后面加上一个生成器后,就实现了生成器的嵌套

当然实现生成器的嵌套,并不是一定必须要使用yield from,而是使用yield from可以让我们避免让我们自己处理各种料想不到的异常,而让我们专注于业务代码的实现

如果自己用yield去实现,那只会加大代码的编写难度,降低开发效率,降低代码的可读性。既然Python已经想得这么周到,我们当然要好好利用起来

讲解它之前,首先要知道这个几个概念

  • 预激活:通过next()方法或send(None)方法使生成器第一次停在yield关键字处,状态由GEN_CREATED变为GEN_SUSPENDED
  • 调用方:调用委派生成器的客户端(调用方)代码
  • 委托生成器:包含yield from表达式的生成器函数
  • 子生成器yield from后面加的生成器函数

直接看代码:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from inspect import getgeneratorstate


# 子生成器
def average_gen():
total, count, average = 0, 0, 0
while True:
new_num = yield average
if not new_num:
break
count += 1
total += new_num
average = total / count
# return 意味着当前协程结束
return total, count, average


# 委托生成器
def proxy_gen():
# 当前协程结束后,进入新的协程并在new_num = yield average处暂停返回给调用方(yield from对子生成器进行预激活)
while True:
sub_coroutine = average_gen()
print(getgeneratorstate(sub_coroutine)) # GEN_CREATED,还未预激活,yield from会进行预激活,使子生成器状态变为GEN_SUSPENDED
total, count, average = yield from sub_coroutine # 这里yield from处理了子生成器return时产生的StopIteration Error
print('sub_coroutine close, stat: {}; total, count, average: {}, {}, {}'
.format(getgeneratorstate(sub_coroutine), total, count, average))


# 调用方
def main():
calc_average = proxy_gen() # 创建委托生成器
next(calc_average) # 预激活委托生成器

print(calc_average.send(10)) # 打印:10.0
print(calc_average.send(20)) # 打印:15.0
print(calc_average.send(30)) # 打印:20.0
calc_average.send(None) # 在子生成器中return,触发StopIteration Error结束当前协程

print(calc_average.send(20))
print(calc_average.send(50))
print(calc_average.send(70))
print(calc_average.send(40))
calc_average.send(None)
calc_average.close()


if __name__ == '__main__':
main()

输出:

1
2
3
4
5
6
7
8
9
10
11
GEN_CREATED
10.0
15.0
20.0
sub_coroutine close, stat: GEN_CLOSED; total, count, average: 60, 3, 20.0
GEN_CREATED
20.0
35.0
46.666666666666664
sub_coroutine close, stat: GEN_CLOSED; total, count, average: 140, 3, 46.666666666666664
GEN_CREATED

上面是一个实时计算平均值的实现,流程这里就不展开讲了,仔细看的都看得清楚,讲一下值得注意的几个点(结合注释):

  • inspect.getgeneratorstate方法可以获取生成器的状态
  • yield from会对子生成器进行预激活
  • 委托生成器只起一个桥梁作用,它建立的是一个双向通道,它并没有权利也没有办法,对子生成器yield回来的内容做拦截
  • yield from会帮我们处理子生成器的所有异常(不只是最常见的StopIteration

总结

yield关键字在Python中可以说很重要了,很多地方的实现都是使用它,尤其在并发编程中,协程的实现也让我们的开发优雅简洁了不少