理解yield关键字、生成器函数和协程
写作背景
主旨:
- 介绍yield关键字的用法、生成器和生成器函数
- 什么是协程、生成器函数和协程的区别
- 从yield到yield from
- future处理并发
- asyncio处理并发
- 异步爬虫实战
很多面试官,上来就是喂你一道协程。很多文章,上来就是教你用asyncio。太难了呀。所以我想写一写自己学到的东西,把有关协程和异步编程的一整个系列写下来。可能会有不对的地方,希望各位看官多多指教。
一、从生成器函数到协程
协程是异步编程的基础,为了理解异步编程,我们就得先搞明白什么是协程。而协程和生成器函数息息相关,因为协程也是一种生成器函数。所以我们再看看生成器函数。
1. yield、生成器和生成器函数
回顾一个例子,demo1()里面使用了yield,是一个生成器函数,返回一个生成器。我们可以使用next()方法来迭代生成器,每次迭代next()获取到的就是yield产生的值,也就是yield右边的值。
# yield可以理解为暂停按钮,每次执行到yield,保存断点,同时yield还会返回值给调用方
def demo1(value=None):
print('start')
yield 1
yield value
yield 2
print('end')
g = demo1("Value") # 生成器函数也是函数,可以接收传参
print(type(g)) # g是一个生成器
print(next(g)) # 执行yield 1,暂停
print(next(g)) # 执行yield value,暂停
print(next(g)) # 执行yield 2,暂停
print(next(g)) # 找不到yield了,raise StopIteration
<class 'generator'>
start
1
Value
2
end
Traceback (most recent call last):
File "D:/Desktop/Projects/效率编程/协程/test.py", line 14, in <module>
print(next(g)) # 找不到yield了,raise StopIteration
StopIteration
总共三次yield,我们是调用方,使用next(g)收到了三个yield返回值,当我们尝试调用第四次时,demo1函数产生了一个StopIteration
告诉我们找不到yield了,raise StopIteration。
2. next()和send(None)
为了将协程,我们先讲讲next()和send()的因缘,next() == send(None)。但是send()是为了传值给生成器而是用的,这个后面会解决
>>> def demo2(value=None):
... print('start')
... a = yield 1
... print("demo1 Received: ", a)
... b = yield value
... print("demo1 Received: ", b)
...
>>> g = demo2("Value")
>>> next(g)
start
1
>>> g = demo2("Value")
>>> g.send(None)
start
1
>>>
为什么说next() == send(None)?我们看看底层代码!不感兴趣的可以跳过。
3. send()和next()的C语言实现
Python底层是由C语言实现的,让我们摘出next()和send()来观察看看!
static PyObject *
gen_iternext(PyGenObject *gen)
{
return gen_send_ex(gen, NULL, 0); # 传入NULL
}
static PyObject *
gen_send(PyGenObject *gen, PyObject *arg)
{
return gen_send_ex(gen, arg, 0); # 传入可变参数
}
4. send() 给生成器函数传值
我们在上面讲到了yield通过yield item
可以返回值给调用方,调用方可以通过next()获取到yield的产出值。其实yield还可以接受调用方传来的值,通过data = yield
,生成器函数内就可以接收调用方传来的值。data = yield item
可以产出一个值item给调用方,同时接收调用方(调用方使用send)传来的值,然后暂停执行,作出让步,使调用方继续工作,直到调用方下次继续执行send。
yield item
:产出值item给调用方。data = yield
:data接收值,下一次send()时,data才会接收到上一次send()的值。data = yield item
:产出值和接收值,先yield item产出值,下一次send()时,data才会接收到上一次send()的值。所以分为yield item 和 data = yield 两步走。
为了理解产出值和接收值,我们看一个产出值和接收值的例子:
def demo2(value=None):
print('start')
a = yield 1
print("demo1 Received: ", a)
b = yield value
print("demo1 Received: ", b)
c = yield 2
print("demo1 Received: ", c)
print('end')
g = demo2("Value")
print(type(g)) # g是一个生成器
print(next(g)) # 执行yield 1,暂停
print(g.send(100)) # 执行yield value,暂停
print(g.send(200)) # 执行yield 2,暂停
print(g.send(300)) # 找不到yield了,raise StopIteration
<class 'generator'>
start
1
demo1 Received: 100
Value
demo1 Received: 200
2
demo1 Received: 300
end
Traceback (most recent call last):
File "D:/Desktop/Projects/效率编程/协程/test.py", line 17, in <module>
print(g.send(300)) # 找不到yield了,raise StopIteration
StopIteration
5. 使用Pycharm的DEBUG模式理解data = yield item的执行过程
还是上面的那段代码,在14行打上断点:
def demo2(value=None):
print('start')
a = yield 1
print("demo1 Received: ", a)
b = yield value
print("demo1 Received: ", b)
c = yield 2
print("demo1 Received: ", c)
print('end')
g = demo2("Value")
print(type(g))
print(next(g)) # 在这里打上断点
print(g.send(100))
print(g.send(200))
print(g.send(300))
-
在Pycharm中,按
Shift+F9
,进入Debug模式,代码运行到13行:
我们发现g是一个’generator’。 -
接下来按
F7
就行了。按F7
首次进入demo2函数内,连续按3次F7之后,控制台打印了’start’和1,此时执行到14行:
-
在运行到第一个send(),也就是第二次进入demo2函数内的时候,a才被赋值100。
-
在运行到第二个send(),第三次进入demo2函数内的时候,b才被赋值200。c同理会在第四次进入demo2函数才被赋值。
二、协程
1. 什么是协程?
什么是协程?这是第一个问题。很多博客和书都在讲协程,但是并没有给出协程定义。也有人把协程叫做“微线程”,下面给出书上的一句话:
协程是指一个过程,这个过程与调用方协作,产出由调用方提供的值。 ——《流程的Python》
Python的协程
,其实就是包含data = yield
的生成器函数,当然也可以是包含data = yield item
。
我们上面讲的例子,demo2()就是一个协程:
def demo2(value=None):
print('start')
a = yield 1
print("demo1 Received: ", a)
b = yield value
print("demo1 Received: ", b)
c = yield 2
print("demo1 Received: ", c)
print('end')
2. 协程的基本操作和四种状态
协程有四种状态,可以使用inspect.getgeneratorstate()确定协程状态:
- ‘GEN_CREATED’:等待开始执行。
- ‘GEN_RUNNIGN’:正在执行。
- ‘GEN_SUSPENDED’:在yield表达式处暂停。
- ‘GEN_CLOSED’:执行结束。
协程(生成器)有四种跟调用方交互的方法():
- next():激活协程(生成器)。执行到第一个yield处暂停,将控制权交给调用方,使协程变为‘GEN_CREATED’状态。
- send():发送数据给协程(生成器),发送的数据会成为yield表达式的值。这一步必须在协程变为‘GEN_CREATED’状态之后使用,也就是必须先使用next()或send(None)激活协程。
- throw():使协程(生成器)在暂停的yield处抛出指定的异常。
- close():终止协程(生成器)。使协程(生成器)在暂停的yield处抛出GeneratorExit异常。
我们看一个例子,使用这四种交互方法,并查看生成器的状态:
from inspect import getgeneratorstate # 导入获取生成器状态的包
class DemoException(Exception): # 自定义一个异常
pass
def demo3():
print('start')
while True:
try:
x = yield
except DemoException:
print("demo3 Received DemoException!")
else:
print("demo3 Received: ", x)
# print('end') # 这一行代码永远不会被执行
- next() 或者 send(None) 预激协程:
>>> c = demo3()
>>> next(c) # 执行第一个yield,暂停
start
>>> getgeneratorstate(c)
'GEN_SUSPENDED'
>>>
>>> c = demo3()
>>> c.send(None)
start
>>> getgeneratorstate(c)
'GEN_SUSPENDED'
- send()
>>> c.send(1)
demo3 Received: 1
>>> c.send(2)
demo3 Received: 2
>>> getgeneratorstate(c)
'GEN_SUSPENDED'
- throw()
>>> c.throw(DemoException) # 调用方丢一个DemoException异常给协程,协程内定义了DemoException的处理方式
demo3 Received DemoException!
>>> getgeneratorstate(c)
'GEN_SUSPENDED'
>>>
>>> c.throw(ValueError) # 调用方丢一个ValueError异常给协程,协程内未定义处理方式
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in demo3
ValueError # 协程将异常返回给调用方
>>> getgeneratorstate(c) # 没办法处理异常会使协程状态变为closed
'GEN_CLOSED'
- close()
>>> c = demo3()
>>> c.close()
>>> getgeneratorstate(c)
'GEN_CLOSED'
我们不能在协程内捕获GeneratorExit异常,否则协程会抛出一个RuntimeError:
def demo3():
print('start')
while True:
try:
x = yield
except GeneratorExit: # 尝试捕获GeneratorExit异常
print("demo3 Received GeneratorExit!")
else:
print("demo3 Received: ", x)
>>> c = demo3()
>>> next(c)
start
>>> c.send(1)
demo3 Received: 1
>>> c.close()
demo3 Received GeneratorExit!
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: generator ignored GeneratorExit
如果想在协程结束时做某些事,可以使用try...finally...
包裹协程:
def demo3():
print('start')
try:
while True:
x = yield
print("demo3 Received: ", x)
finally:
print('end')
>>> c = demo3()
>>> next(c)
start
>>> c.send(1)
demo3 Received: 1
>>> c.close()
end # 调用close()后,输出了end
3. 个人对yield和协程的理解
3.1 如何理解协程
普通的函数,都是等待接收方调用,调用一次执行(接收参数、执行函数体、返回值)就结束了。如果有一个函数,在一次调用内,既能够接收你传入的值n次,又能返回值m次给你,在执行过程中,可以暂停等待你传值,直到你需要再次启动,是不是感觉有点牛逼?没错,我们前面看到的,Python里yield关键字
不就是做这件事的吗?data = yield item
可以产出一个值item给调用方,同时接收调用方(调用方使用send)传来的值,然后暂停执行,作出让步,使调用方继续工作,直到调用方下次继续执行send。
总之,data = yield item
执行过程可以理解为产出值、暂停、接收值上一次send传来的值,依次反复。
3.2 协程FAQ
一问:协程跟生成器是什么关系?
协程是生成器的一种,协程是一种控制流程,四种交互方法本质就是调用方跟生成器交互,因为生成器函数返回的就是生成器,而协程也是由生成器函数返回的。
二问:为什么要先使用next()预激活协程?
说了协程是一种控制流程。假如你雇了一个人给你装修房子,你愿意在你还没允许的情况下这个人就开始装修吗?所以,调用方得先使用next()告诉协程,我要你准备开始跑了,你才能开始。
3.3 区别生成器函数和协程
- 包含
yield
关键字的函数就是生成器函数
。 - 通过
data = yield
生成器函数内就可以接收调用方传来的值,接收值的生成器函数,就可以理解为Python的协程
。
4. 使用协程实现闭包
普通的闭包:我们定义一个嵌套函数来实现数据的加和,外层函数定义一个total变量来记录前面的和:
def add():
total = 0.0
def calculate(x):
nonlocal total
total += x
return total
return calculate
a = add()
print(a(1)) # 1.0
print(a(2)) # 3.0
print(a(3)) # 6.0
使用协程:
def add():
total = 0.0
while True:
term = yield total
total += term
>>> a = add()
>>> next(a) # 调用next函数,预激协程
0.0
>>> a.send(1) # 多次调用send(),计算总和
1.0
>>> a.send(2) # 多次调用send(),计算总和
3.0
>>> a.send(3) # 多次调用send(),计算总和
6.0
>>>
5. 让协程返回值
我们想让协程跟普通函数一样,通过return返回值给我们。首先抛出一个疑问,既然调用方可以通过yield接收协程返回的值,我们为什么还要让协程返回值通过return返回值给我们呢?这是因为很多协程不会通过yield产出值,而是在最后返回一个值(比如累计值)。改写上面的累加函数:
def add():
total = 0.0
while True:
term = yield # yield不再产出值
if term is None: # 让协程接收None时结束while True循环
break
total += term
return {'total': total}
>>> a = add()
>>> next(a)
>>> a.send(1)
>>> a.send(2)
>>> a.send(3)
>>> a.send(None) # send(None)使协程结束while
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: {'total': 6.0} # 抛出了StopIteration
为什么会抛出StopIteration?生成器结束的时候就会抛出StopIteration,如果还不理解,请回去看生成器的内容。
我们可以使用try…except…捕获这个StopIteration:
>>> a = add()
>>> next(a)
>>> a.send(1)
>>> a.send(2)
>>> a.send(3)
>>> try:
... a.send(None)
... except StopIteration as e:
... result = e.value
...
>>> result
{'total': 6.0}
在函数外面写try…except…真的是看得人头疼,所以尝试定义一个函数,帮助我们完成try…except…,但是这个函数又要能够跟生成器一样暂停和接收值,所以这个函数注定是一个生成器函数,是一个协程。
下面实现了一个例子,我们把add()叫做子生成器,把grouper()叫做委派生成器,这俩都是协程:
def add():
total = 0.0
while True:
term = yield
if term is None:
break
total += term
return {'total': total}
def grouper(result):
_a = add()
next(_a) # 预激子生成器
while True:
x = yield # yield必不可少,接收调用方的值
try:
_a.send(x) # 委派生成器接收调用方的值后,要把值传给子生成器
except StopIteration as e: # 我们只捕获子生成器的StopIteration
res = e.value
result.append(res)
# return res # 为什么不返回?
if __name__ == '__main__':
result = []
g = grouper(result)
next(g) # g本身也是一个生成器,需要预激活
g.send(1)
g.send(2)
g.send(3)
g.send(None)
print(result)
[{'total': 6.0}]
委派生成器grouper帮我们完成了两件事,第一,预激活子生成器(协程),第二,捕获StopIteration的返回值。但是grouper本身也是一个生成器,需要预激活,所以对于调用方而言,唯一不用关心的,就是如何捕获StopIteration的返回值了。
为什么委派生成器grouper不使用返回值,我在这里讲一下自己的理解:
因为grouper也是一个生成器函数,如果返回,则会抛出StopIteration,我们又需要在外部处理StopIteration。而如果不返回,grouper会暂停在yield处,调用方不需要处理StopIteration。为了获取子生成器add的返回值,调用方可以传入可变对象,注意,一定是可变对象。因为可变对象传参,函数修改的是原来的可变对象,而如果是不可变对象传参,函数会拷贝一份不可变对象的值,不会修改原来的不可变对象。如果不是很理解,请自行百度可变对象传参和不可变对象传参的区别。
6. yield from
好家伙,很多文章博客,上来就是yield from贴脸,搞得我一脸懵逼~~~下面讲讲从yield过渡到yield from。
上面的示例,grouper的代码太多了吧!看得头疼,yield from 就是为了解决这个问题的,使用yield from简化grouper的代码:
def grouper(result):
while True:
res = yield from add()
result.append(res)
[{'total': 6.0}]
运行结果和前面是一样的。是不是清爽很多?
yield from的作用,是为了获取子生成器的返回值。假如你在委派生成器grouper内return,调用方依旧会接收到StopIteration。所以最终,总得有人处理这个StopIteration的,或者干脆不抛出StopIteration。
def add():
total = 0.0
while True:
term = yield
if term is None:
break
total += term
return {'total': total}
def middle():
res = yield from add()
return {'result': res}
def grouper():
res = yield from middle()
return res
if __name__ == '__main__':
result = []
g = grouper()
next(g)
g.send(1)
g.send(2)
g.send(3)
try:
g.send(None)
except StopIteration as e:
print(e.value)
7. 再使用yield from实现多层委派生成器
上面的返回是[{'total': 6.0}]
,我们加个中间层的委派生成器,使得返回值变为[{'result': {'total': 6.0}}]
:
def add():
total = 0.0
while True:
term = yield
if term is None:
break
total += term
return {'total': total}
def middle(): # 细节1:中间的委派生成器一定要返回值
res = yield from add()
return {'result': res} # return
def grouper(result): # 接收一个可变对象用于保存返回值
while True: # 细节2:末层的委派生成器使用while True循环,不return抛出异常
res = yield from middle()
result.append(res)
if __name__ == '__main__':
result = []
g = grouper(result)
next(g)
g.send(1)
g.send(2)
g.send(3)
g.send(None)
print(result)
[{'result': {'total': 6.0}}]
委派生成器相当于管道,可以把任意数量个委派生成器连接在一起:一个委派生成器使用 yield from 调用一个子生成器,而那个子生成器本身也是委派生成器,使用 yield from 调用另一个子生成器,以此类推。最终,这个链条要以一个只使用 yield 表达式的简单生成器结束。
8. 使用协程模拟出租车队运营程序
终于来到这里了,使用协程实现出租车程序调度,帮助我们理解协程的调度是如何实现并发的。前面内容写累了,此处就不做讲解了,直接翻书看,例子来自《流畅的Python》。贴上代码和运行结果:
import random
import collections
import queue
DEFAULT_NUMBER_OF_TAXIS = 3
DEFAULT_END_TIME = 180
SEARCH_DURATION = 5
TRIP_DURATION = 20
DEPARTURE_INTERVAL = 5
Event = collections.namedtuple('Event', 'time proc action')
# BEGIN TAXI_PROCESS
def taxi_process(ident, trips, start_time=0): # <1>
"""Yield to simulator issuing event at each state change"""
time = yield Event(start_time, ident, 'leave garage') # <2>
for i in range(trips): # <3>
time = yield Event(time, ident, 'pick up passenger') # <4>
time = yield Event(time, ident, 'drop off passenger') # <5>
yield Event(time, ident, 'going home') # <6>
# end of taxi process # <7>
# END TAXI_PROCESS
# BEGIN TAXI_SIMULATOR
class Simulator:
def __init__(self, procs_map):
self.events = queue.PriorityQueue()
self.procs = dict(procs_map)
def run(self, end_time): # <1>
"""Schedule and display events until time is up"""
# schedule the first event for each cab
for _, proc in sorted(self.procs.items()): # <2>
first_event = next(proc) # <3>
self.events.put(first_event) # <4>
# main loop of the simulation
sim_time = 0 # <5>
while sim_time < end_time: # <6>
if self.events.empty(): # <7>
print('*** end of events ***')
break
current_event = self.events.get() # <8>
sim_time, proc_id, previous_action = current_event # <9>
print('taxi:', proc_id, proc_id * ' ', current_event) # <10>
active_proc = self.procs[proc_id] # <11>
next_time = sim_time + compute_duration(previous_action) # <12>
try:
next_event = active_proc.send(next_time) # <13>
except StopIteration:
del self.procs[proc_id] # <14>
else:
self.events.put(next_event) # <15>
else: # <16>
msg = '*** end of simulation time: {} events pending ***'
print(msg.format(self.events.qsize()))
# END TAXI_SIMULATOR
def compute_duration(previous_action):
"""Compute action duration using exponential distribution"""
if previous_action in ['leave garage', 'drop off passenger']:
# new state is prowling
interval = SEARCH_DURATION
elif previous_action == 'pick up passenger':
# new state is trip
interval = TRIP_DURATION
elif previous_action == 'going home':
interval = 1
else:
raise ValueError('Unknown previous_action: %s' % previous_action)
return int(random.expovariate(1/interval)) + 1
def main(end_time=DEFAULT_END_TIME, num_taxis=DEFAULT_NUMBER_OF_TAXIS,
seed=None):
"""Initialize random generator, build procs and run simulation"""
if seed is not None:
random.seed(seed) # get reproducible results
taxis = {i: taxi_process(i, (i+1)*2, i*DEPARTURE_INTERVAL)
for i in range(num_taxis)}
sim = Simulator(taxis)
sim.run(end_time)
if __name__ == '__main__':
main()
taxi: 0 Event(time=0, proc=0, action='leave garage')
taxi: 1 Event(time=5, proc=1, action='leave garage')
taxi: 0 Event(time=6, proc=0, action='pick up passenger')
taxi: 1 Event(time=6, proc=1, action='pick up passenger')
taxi: 0 Event(time=8, proc=0, action='drop off passenger')
taxi: 0 Event(time=9, proc=0, action='pick up passenger')
taxi: 2 Event(time=10, proc=2, action='leave garage')
taxi: 2 Event(time=13, proc=2, action='pick up passenger')
taxi: 2 Event(time=14, proc=2, action='drop off passenger')
taxi: 0 Event(time=16, proc=0, action='drop off passenger')
taxi: 2 Event(time=17, proc=2, action='pick up passenger')
taxi: 0 Event(time=19, proc=0, action='going home')
taxi: 2 Event(time=19, proc=2, action='drop off passenger')
taxi: 1 Event(time=22, proc=1, action='drop off passenger')
taxi: 1 Event(time=26, proc=1, action='pick up passenger')
taxi: 1 Event(time=31, proc=1, action='drop off passenger')
taxi: 1 Event(time=32, proc=1, action='pick up passenger')
taxi: 1 Event(time=39, proc=1, action='drop off passenger')
taxi: 2 Event(time=39, proc=2, action='pick up passenger')
taxi: 1 Event(time=42, proc=1, action='pick up passenger')
taxi: 1 Event(time=55, proc=1, action='drop off passenger')
taxi: 1 Event(time=56, proc=1, action='going home')
taxi: 2 Event(time=63, proc=2, action='drop off passenger')
taxi: 2 Event(time=66, proc=2, action='pick up passenger')
taxi: 2 Event(time=92, proc=2, action='drop off passenger')
taxi: 2 Event(time=105, proc=2, action='pick up passenger')
taxi: 2 Event(time=155, proc=2, action='drop off passenger')
taxi: 2 Event(time=156, proc=2, action='pick up passenger')
taxi: 2 Event(time=171, proc=2, action='drop off passenger')
taxi: 2 Event(time=175, proc=2, action='going home')
*** end of events ***
期待再会…
[Python] 使用futures模块处理并发(超好用的并发库)
future和asyncio还没讲完,后续内容会慢慢更新。