HackPython致力于有趣有价值的编程教学
简介
在上一篇,讨论了阻塞/非阻塞、同步/异步、并发/并行等概念,本节主要来讨论一下生成器、yield以及yield from概念并进行简单的使用。
关键概念
Python中利用了asyncio这个标准库作为异步编程框架,而aysncio以及其他多数协程库内部都大量使用了生成器,所以先从生成器聊起。为什么会是生成器🤔?回想一下生成器的特性,其利用了yield关键字做到了随时暂停以及随时执行的能力,而协程从技术实现角度而言,它的作用其实就是一个可以随时暂停会执行的函数。
生成器
生成器与迭代器关系紧密,😗其实生成器就是迭代器另一种更优雅的实现方式,其利用了yield关键字实现了迭代器的功能,生成器可以迭代式的利用内存空间,让数据在需要使用时才被载入,这减少内存的消耗,其利用yield关键字使用了这个功能,当生成器函数执行过程中遇到yield就会被展厅执行,等下次迭代时再次从暂停处继续执行。
为了让生成器可以实现简单的协程,🤩在Python 2.5 的时候对生成器的能力进行了增强,此时利用yield可以暂停生成器函数的执行返回数据,也可以通过send()方法向生成器发送数据,并且还可以利用throw()向生成器内抛出异常以实现可随时终止生成器的目的。
yield的作用直观如下图:
从图中可看出,在一开始调用simple_coro2()方法时,获得的my_coro2变量并不是具体的值,而是一个生成器对象,此时调用其next()方法进行迭代,next()方法会让生成器函数执行到yield处,到yield后就会会将紧随在其后的变量返回,接着可以利用send()方法将值传递到生成器中,并让暂停的函数继续从暂停处执行😏,next()与send()的不同之处在于next()并不能向生成器内部传递值而send()可以,可以直接使用send(None)来实现next()方法的效果。从图中也可以看出,next()与send()会获得下一个yield返回的值。
顺带一提,for迭代也调用了迭代器中的__next__方法,next()内部也是该方法🤫。
yield from
为了让生成器分成多个子生成器后可以很容易使用next()、send()、throw()等方法,Python3.3中引入了yield from语言🤩,它允许将一个生成器的部分操作委派给另一个生成器。
虽然yield from设计的目的是为了让生成器本身可以委派给子生成器,但yield from可以向任意可迭代对象进行委派操作🤭。
yield from iterable 本质其实就是 for item in iterable: yield item,只是写法更优雅了,简单使用如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17In [1]: def gen1():
...: for i in 'abc':
...: yield i
...: for i in range(5):
...: yield i
...:
In [2]: list(gen1())
Out[2]: ['a', 'b', 'c', 0, 1, 2, 3, 4]
In [4]: def gen2():
...: yield from 'abc'
...: yield from range(5)
...:
In [5]: list(gen2())
Out[5]: ['a', 'b', 'c', 0, 1, 2, 3, 4]
上述代码中其实涉及几个概念,其中gen2()方法因为包含了yield from表达式,所以被称为😀委派生成器,而yield from后接着的表达式通常称为😀子生成器,上述代码中的’abc’,range(5)都是子生成器,而滴啊用委派生成器的代码称为😀调用方。
此外,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
50from collections import namedtuple
Result = namedtuple('Result', 'count average')
# the subgenerator
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield
print('term:', term)
if term is None:
break
total += term
count += 1
average = total / count
return Result(count, average)
# the delegating generator
def grouper(results, key):
while True:
#只有当生成器averager()结束,才会返回结果给results赋值
results[key] = yield from averager()
print('resluts[key]:', results[key])
def report(results):
for key, result in sorted(results.items()):
group, unit = key.split(';')
print('{:2} {:5} averaging {:.2f}{}'.format(result.count, group, result.average, unit))
def main(data):
results = {}
for key, values in data.items():
group = grouper(results, key)
print(type(group))
next(group)
for value in values:
r = group.send(value)
print('r:',r)
print('value:',value)
group.send(None)
report(results)
data = {
'girls;kg':[40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],
'girls;m':[1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],
'boys;kg':[39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],
'boys;m':[1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46],
}
if __name__ == '__main__':
main(data)
在上述代码中,grouper函数是委托生成器😗,averager函数是子生成器😗,而main()函数就是调度者😗。
在main()函数中,首先通过grouper()获得对应的生成器对象,然后调用next()方法进行初步的迭代,此时会执行到averager()的yield处,因为yield后没有跟对应的变量,则yield返回的值为None,该值会有grouper()委托生成器直接传递给main()调度者,观察变量r的打印则可,接着for迭代中使用委托生成器的send()方法,该方法发送的数据会有委托生成器直接传递给子生成器,即averager()函数中term的值,上述代码调度的关系如下图:
从图中看出,😐调度者使用send()方法传递的数据会被委派生成器直接传递给子生成器,而子生成器yield的方法数据也被直接传递会调度者,如果子生成器产生StopIteration异常则表示子生成器已经迭代完了,此时委派生成器会接收到该异常,从而继续执行yield from整个表达式后的其他表达式,这里grouper()函数中yield from执行完后,就没有逻辑了。
可以看出,委派生成器具有组织多个子生成器的能力,并将调度者的信息转手传递给子生成器😯。
结尾
在本节中,主要介绍Python中生成器、yield以及yield from的概念与使用,在下一篇中,会接着讨论Python的asyncio框架以及async/await原生协程,最后欢迎学习HackPython的教学课程并感谢您的阅读与支持。
参考文章