从生成器到协程
协程的概念
- 协程不是进程或线程,其执行过程更类似于子例程,或者说不带返回值的函数调用。
- 一个程序可以包含多个协程,可以对比与一个进程包含多个线程,因而下面我们来比较协程和线程。我们知道多个线程相对独立,有自己的上下文,切换受系统控制;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其他协程由当前协程来控制。
- 从上述字面上的理解为,多线程的暂停与启动是操作系统来控制的,比如gil等。协程是单线程,在单个线程里面能够有能暂停,启动和向函数中去传递值的功能我们就叫做协程。
- python中 生成器 yield正好有这个暂停和启动的功能。我们可以思考使用生成器来作为协程使用。
生成器的基本行为
- 控制生成器
def simple_gen1():
print('started')
x = yield 'running' #调用生成器
print('ending',x)
test1 = simple_gen1()
print(next(test1)) #执行 yield之前的语句和 yield后面的“running”
test1.send(1) #传值1 给 yield的左边x,并执行下面的语句,当执行完毕所有语句时,
#会包 StopIteration 异常,可用try-except语句避免报错
#(输出)
Traceback (most recent call last):
File "C:/Users/admin/PycharmProjects/untitled1/asd.py", line 9, in <module>
started
running
ending 1
test1.send(1)
StopIteration #如果没有下一个元素则会触发 StopIteration 异常,可用try-except语句避免报错
next 语法:
next(iterator[, default])
参数说明:
iterator – 可迭代对象
default – 可选,用于设置在没有下一个元素时返回该默认值,如果不设置,又没有下一个元素则会触发 StopIteration 异常。
def simple_gen1():
print('started')
x = yield 'running' # 调用生成器
print('ending', x)
test1 = simple_gen1()
print(next(test1)) #打印的是running,相当于函数的return的值,即yield的右边语句
next(test1)
#(输出)
started
Traceback (most recent call last):
running
File "C:/Users/admin/PycharmProjects/untitled1/asd.py", line 9, in <module>
ending None
next(test1)
StopIteration
添加 default 参数:
test1 = simple_gen1()
print(next(test1))
next(test1,'ZXC')
print(next(test1,'ZXC'))
#(输出)
started
running
ending None
ZXC
Process finished with exit code 0
-
代码解释
-
yield的右边表示生成一个值,但其实yield左边可以表示接受调用方传递的值,只不过我们一般不会书写,所以它会默认的传递None
-
调用next方法去启动生成器。启动之后生成器会在第一个yield的暂停住,并且生成出一个值,值是 yield表达式右边的值。
-
调用send方法,yield左边会接受值,并且向下执行,如果没有yield暂停则会抛出stopItertion的异常出来
-
通过上面的代码我们看出,生成器可以暂停,和接受值。他有协程的特征存在。
-
-
查看生成器的状态
from inspect import getgeneratorstate def simple_gen1(): print('started') x = yield 'running' print('receive_x:',x) y = yield x print('receive_y:', y) test1 = simple_gen1() print(getgeneratorstate(test1)) next(test1) print(getgeneratorstate(test1)) next(test1) print(getgeneratorstate(test1)) try: #跳过上面的StopIteration 异常 next(test1) except Exception: pass print(getgeneratorstate(test1))
输出
GEN_CREATED #未激活 started GEN_SUSPENDED #暂停中 receive_x: None GEN_SUSPENDED #暂停中 receive_y: None GEN_CLOSED #已经结束
通过上面的代码,我们可以看到 生成器是有状态的—>
未激活,暂停中,已经结束 -
运行测试:
def coro(a):
print('res:', str(a))
b = yield a
print('res', str(a + b))
c = yield b
print('res', str(a + c))
try:
test1 = coro(5) #不会输出,因为是生成器对象
next(test1) #执行第一个yield以上的语句和右边语句,输出print的 5
test1.send(6) #传递值6给yield的右边b,并执行下面语句,知道遇到第二个yield,输出11
test1.send(9) #传递值9给yield的右边c,并执行下面语句。输出14
except:
pass
#(输出)
res: 5
res 11
res 14
-
我们可以认为,使用yield这种关键字是使用了python的协程,yield既可以作为生成器生成值的关键字,也可以作为python里面的协程使用
-
协程的用法:
- next或者send(none)去激活协程,使他移动到yield处停止
- 使用 send 给协程发送值过去,协程向下执行
- 使用next就不会发送值,或者发送的值是none。协程向下执行
- 使用close可以主动的停止协程
生成器小运用
- 一个无限的平均值计算器
# 无限的计算平均值
# 输入: 1. 5-->5 2. 3 --->8 3. 2--->10 4. 2---->12
# 输出: 1. 5 2. 4 3. 3.333 4. 3
def avg():
num = 0
total = 0.0
while 1: # 无限的循环
try: # 接收一个值
val = yield
num += 1 # 计算次数+1
total += val
avg = total / num # 平均数等于总数/计算次数
print('当前平均数{}'.format(avg))
except IndexError:
print('当前没有值')
except Ex:
print('停止错误')
return 5 #return会关闭生成器
class Ex(Exception):
pass
counter = avg()
counter.send(None)
# send 错误
while True:
num = input('请输入数字:')
try:
num = float(num)
except Exception:
try:
counter.throw(Ex)
except StopIteration as e:
print('生成器已经停止')
print(e.value)
continue
counter.send(num)
#return 他可以向我们这边抛出stopiteration
#return 他可以像我们返回值
#return 关闭我们的生成器
#协程返回值是放在 stopiteration
#(输出)
请输入数字:123
当前平均数123.0
请输入数字:sfgdf'
停止错误
生成器已经停止
5
请输入数字:12 #生成器已经关闭,再次输入就会报错了
Traceback (most recent call last):
File "C:/Users/admin/PycharmProjects/untitled1/阿达.py", line 43, in <module>
counter.send(num)
StopIteration
> 协程会不断的接受值,然后再yield处生成计算结果,并且暂停等待新的send值过来再循环
异常处理
- 协程内部发生异常之后,会向上冒泡,正如我们之前所看到的,并且会终止协程。终止之后的协程无法重新激活
- 我们可以使用 throw向协程内部发送异常,并且协程可以catch(抓住)异常。
class DemoException(Exception):
pass
def test1():
print('start')
while True:
try: # 异常处理
x = yield
except DemoException:
print('demo ex')
else:
print('receive:', x)
t = test1()
next(t)
t.send(6)
t.throw(DemoException)
t.send(9)
#(输出)
start
receive: 6
demo ex
receive: 9
>我们可以看到,协程抓住了异常并且继续正常运行
-
close终止协程
我们可以看到,close方法会给协程发送StopIteration异常,致使协程停止
class DemoException(Exception):
pass
def test1():
print('start')
while True:
try:
x = yield
except DemoException:
print('demo ex')
except StopIteration:
print('asdf')
else:
print('receive:', x)
t = test1()
next(t)
t.send(6)
t.throw(DemoException)
t.send(9)
t.throw(StopIteration)
t.send(20)
#(输出)
start
receive: 6
demo ex
receive: 9
asdf
receive: 20
返回值
- 使用return来返回值
- 但是,普通的终止下,协程只会返回None
- 返回的值在StopIertion中
def sum():
avg = None
total = 0.0
num = 0
while True:
r = yield avg
if r is None:
break
total += r
num += 1
avg = total/num
return num,avg
s1 = sum()
next(s1)
s1.send(5)
s1.send(8)
s1.send(10)
try:
s1.send(None)
except Exception as e:
print(e.value)
#(输出)
(3, 7.666666666666667)
测试
import time
import random
def consume():
while True:
product = yield #新建生成器
print('consume product:',product)
if product == 5:
return ('收到5')
else:
print("没有找到5")
def product():
t = consume()
next(t) #启动生成器,如果不先启动,再进行send操作
#将会报错TypeError: can't send non-None value to a just-started generator
while True:
try:
time.sleep(random.randint(0,1)) #产生随机数,
t.send(random.randint(1,6)) #传递值进行验证
except StopIteration as e: #避免错误
print(e.value)
break
if __name__ == "__main__":
product()
#(输出)
consume product: 6
没有找到5
consume product: 5
收到5
模仿出租车程序
模仿出租车在120 分钟之内的随机情况
事件驱动:
1.出发
2.载客
3.出发
4.载客
。。。。。。
n.回家睡觉
根据时间进行,多辆车进行
import collections #集合模块,提供了许多有用的集合类。
import queue #队列模块
import random #随机数
Event = collections.namedtuple('Event', 'time proc action') #namedtuple给tuple起个名字,有tuple元祖的性质
#也有dict字典性质,key-value取值,还有object对象的性质
#第一个参数必须和变量一样,第二个参数定义数值
#time表示出租车在什么时间做什么,
#proc表示 那一辆出租车 即id
#action表示事件,如出发,载客,回家
def taxi_process(ident, trips, start_time=0): #taxi代表出租车的事件循环,默认出发时间为零分钟,
# trips载客次数 ident出租车id
time = yield Event(start_time, ident, 'leave garage') #出发时间
for i in range(trips):
time = yield Event(time, ident, 'pick up passenger接客时间') #接客时间
time = yield Event(time, ident, 'drop off passenger送达目的') #送达目的地下客
yield Event(time, ident, 'going home回家') #回家
class Simulator:
"""
出租车数量
"""
def __init__(self,taxis):
self.events = queue.PriorityQueue() #优先队列,根据的是第一个参数判断优先级排序
self.taxis = taxis.copy()
def run(self,end_time): #激活出租车(生成器)
for k, i in self.taxis.items():
first_event = next(i) #第一个事件
self.events.put(first_event) #添加事件
sim_time = 0
while sim_time < end_time:
if self.events.empty():
print('end events')
break
current_event = self.events.get() #从优先队列中取事件,根据时间先后排序取值
sim_time,taxi_id,actions = current_event #tuple元祖拆包,比如 a,b,c = (1,2,3)
print('taxi:',taxi_id,'at:',sim_time,'do',current_event) #打印
next_time = sim_time + random.randint(1,8) #下一个事件的时间
try:
next_event = self.taxis[taxi_id].send(next_time)
except StopIteration:
del self.taxis[taxi_id] #删除出租车id
else:
self.events.put(next_event) #添加出租车id和时间 到优先队列queue.PriorityQueue中
else:
print('time out时间段内事件结束')
def main(taxi_num):
taxis = dict() #建立一个字典,出租车id对应 出租车信息id,载客数,开始时间
for i in range(taxi_num): #出租车的id
#生成出租车的对象,间隔发车,运营次数随机
taxis[i] = taxi_process(i, random.randint(10,20), start_time=random.randint(0,10))
sim = Simulator(taxis) #构建对象sim,传递参数
sim.run(120) #调运run,运行时间120
if __name__ == '__main__':
# taxi = taxi_process(ident=5,trips=2,start_time=7)
# print((next(taxi)))
# print(taxi.send(9))
# print(taxi.send(14))
# print(taxi.send(24))
# print(taxi.send(34))
# print(taxi.send(44))
# print(taxi.send(54))
# print(taxi.send(64))
main(9) #出租车数量
优化版
import collections #集合模块,提供了许多有用的集合类。
import queue
import random
class Simulator:
def __init__(self):
self.events = queue.PriorityQueue() #优先队列,根据的是第一个参数判断优先级排序
self.taxis = {}
self.Event = collections.namedtuple('Event', 'time proc action')
#namedtuple给tuple起个名字,有tuple元祖的性质
# taxi代表出租车的事件循环,默认出发时间为零分钟
# trips载客次数 ident出租车id
def taxi_process(self,ident, trips, start_time=0):
time = yield self.Event(start_time, ident, 'leave garage:出发时间') # 出发时间
for i in range(trips):
time = yield self.Event(time, ident, 'pick up passenger:接客时间') # 接客时间
time = yield self.Event(time, ident, 'drop off passenger:送达目的') # 送达目的地下客
yield self.Event(time, ident, 'going home回家') # 回家
def run(self,end_time): #激活出租车(生成器)
for k, i in self.taxis.items():
first_event = next(i) #第一个事件
self.events.put(first_event) #添加事件
sim_time = 0
while sim_time < end_time:
if self.events.empty(): #Queue.empty()如果队列为空,返回True,反之False,
# 可设置出租车数量为零时触发
print('end events')
break
current_event = self.events.get() #从优先队列中取事件,根据时间先后排序取值
sim_time,taxi_id,actions = current_event #tuple元祖拆包,比如 a,b,c = (1,2,3)
print('taxi:',taxi_id,'at:',sim_time,'do',current_event) #打印
next_time = sim_time + random.randint(1,8) #下一个事件的时间
try:
next_event = self.taxis[taxi_id].send(next_time) #从taxis字典里找到对应的出租车进行send
#send 给协程发送值过去,协程向下执行
except StopIteration:
del self.taxis[taxi_id] #删除出租车id
else:
self.events.put(next_event) #添加出租车id和时间 到优先队列queue.PriorityQueue中
else:
print('time out时间段内事件结束')
def main(self,taxi_num):
for i in range(taxi_num): # 出租车的id
# 生成出租车的对象,间隔发车,运营次数随机
self.taxis[i]=self.taxi_process(i, random.randint(10, 20), start_time=random.randint(0, 10))
self.run(120) # 调运run,运行时间120
if __name__ == '__main__':
ss = Simulator()
ss.main(2) #出租车数量