Python协程详解(一)
yield有两个意思,一个是生产,一个是退让,对于Python生成器的yield来说,这两个含义都成立。yield这个关键字,既可以在生成器中产生一个值,传输给调用方,同时也可以从调用方那获取一个值,在生成器内部使用。此外,yield还会作出让步,暂停生成器,让调用方继续工作,直到调用方需要下一个数据时,调用方则陷入等待直到成器提供给调用方所需的数据,如此循环往复。乍一听,有点像多线程,不明白多线程的同学也不要紧张,可以简单的解释一下多线程
解释多线程之前,我们先解释一下进程,进程可以看成是电脑里运行的一个实例,比方说,我运行一个浏览器,是一个进程,运行QQ,同样也是进程,我用浏览器浏览网站,用浏览器听音乐和下载东西,可以看成浏览器这个进程,里面有3个线程同时在为我做浏览网站,播放音乐还有下载文件。而我用QQ和人聊天,同时我又用QQ给人传输文件,同样也是在QQ这个进程中,有两个线程在为我传输聊天内容,同时传输文件。当然,上述说的并不严谨,只是为了好理解,因为对于像浏览器或者QQ这样的进程,每时每秒可能有成百上千的线程在运行,有可能记录日志或者其他
而协程相比于线程,最大的区别在于,协程不需要像线程那样来回的中断切换,也不需要线程的锁机制,因为线程中断或者锁机制都会对性能问题造成影响,所以协程的性能相比于线程,性能有明显的提高,尤其在线程越多的时候,优势越明显
下面用个例子来看一下协程的运作:
def simple_coroutine():
for i in range(3):
x = yield i + 1 # <1>
print("从调用方获取的值:%s" % x) my_coro = simple_coroutine() # <2>
first = next(my_coro) # <3>
for i in range(5): # <4>
try:
y = my_coro.send(i) # <5>
print("从生成器中获取的值:%s" % y)
except StopIteration:
print("生成器的值拉取完毕") # <6>
print("生成器最初获取的值:%s" % first)
运行结果:
从调用方获取的值:0
从生成器中获取的值:2
从调用方获取的值:1
从生成器中获取的值:3
从调用方获取的值:2
生成器的值拉取完毕
生成器的值拉取完毕
生成器的值拉取完毕
生成器最初获取的值:1
我们先来说一下程序的运行过程,先看程序中<2>处的代码,传统的概念中,我先执行了my_coro = simple_coroutine() 这块代码,所以会理所当然的认为, simple_coroutine() 这个方法要先执行完毕才能接着执行后续的代码,但实际上不是,因为yield会标明这个方法是一个生成器,所以在程序的最初,他不会先执行完毕 simple_coroutine() 方法,而是把my_coro 这个变量声明称一个生成器,跳过simple_coroutine() ,接着执行后续代码
Python将my_coro 声明称一个生成器后,调用了<3>处的next(my_coro) ,这个方法才开始会顺序执行simple_coroutine()方法中的代码,在Python解释器执行simple_coroutine()方法时,遇到yield关键字,生成器将会陷入等待,这时候解释器会跳到生成器之外,也就是<3>之后的代码,我们调用生成器的send()方法,并传输一个值,这时候Python解释器会从外部的代码重新跳回simple_coroutine() 方法,中在之前停留的地方继续执行
他会将外部传来的值赋给x变量,并顺序执行,直到遇到下一个yield,再像之前那样跳出方法外
由于生成器能提供的值有限,所以当simple_coroutine()方法中执行了3次循环,生成器已经没有多余的值可供调用方获取了,所以每次调用生成器的send()方法,都会抛出StopIteration异常
这里有一点要注意,要激活一个生成器,一定要调用next()方法,而不是调用生成器的send()方法,如果直接调用send()方法会报错
协程有四种状态,分别是
GEN_CREATED:等待执行
GEN_RUNNING:解释器执行
GEN_SUSPENDED:在yield表达式处暂停
GEN_CLOSED:执行结束
协程的状态可以用inspect.getgeneratorstate()函数来确定,来看下面的例子:
from inspect import getgeneratorstate
from time import sleep
import threading def get_state(coro):
print("其他线程生成器状态:%s", getgeneratorstate(coro)) # <1> def simple_coroutine():
for i in range(3):
sleep(0.5)
x = yield i + 1 # <1> my_coro = simple_coroutine()
print("生成器初始状态:%s" % getgeneratorstate(my_coro)) # <2>
first = next(my_coro)
for i in range(5):
try:
my_coro.send(i)
print("主线程生成器初始状态:%s" % getgeneratorstate(my_coro)) # <3>
t = threading.Thread(target=get_state, args=(my_coro,))
t.start()
except StopIteration:
print("生成器的值拉取完毕")
print("生成器最后状态:%s" % getgeneratorstate(my_coro)) # <4>
执行结果:
生成器初始状态:GEN_CREATED
生成器状态:%s GEN_SUSPENDED
生成器状态:%s GEN_SUSPENDED
生成器的值拉取完毕
生成器的值拉取完毕
生成器的值拉取完毕
生成器最后状态:GEN_CLOSED
<2>处,在激活协程之前,协程的状态是GEN_CREATED,而执行next()之后,以及在调用生成器send()之间,我分主线程也就是调用方和多线程去观察协程的状态,结果状态都是GEN_SUSPENDED,也就是协程处于暂停的状态,我原本想用多线程去捕捉协程的运行态,结果即便是多线程捕捉协程也是GEN_SUSPENDED,而GEN_RUNNING也说明,只有带解释器在运行协程的时候,协程的状态才是GEN_RUNNING,最后是GEN_CLOSED,我们拉取完协程的值后,协程的状态就变为执行结束
示例:使用协程计算平均值
我们可以开发一个协程,不断的往协程发送值,并且让协程累计之前的值并计算平均值,如下:
from functools import wraps def coroutine(func):
@wraps(func)
def primer(*args, **kwargs):
gen = func(*args, **kwargs)
next(gen)
return gen return primer @coroutine # <1>
def averager():
total = .0
count = 0
average = None
while True:
term = yield average
total += term
count += 1
average = total / count try:
coro_avg = averager()
print(coro_avg.send(10))
print(coro_avg.send(20))
print(coro_avg.send(30))
coro_avg.close() # <2>
print(coro_avg.send(40))
except StopIteration:
print("协程已结束")
运行结果:
10.0
15.0
20.0
协程已结束
在<1>处,我们用一个装饰器来预先激活协程,而不是之后再调用方里执行一个next()函数。然后,我们不断往协程里传10、20、30,而协程不断累计传入的值,并计算所有值的平均值返回给调用方,最后,我们在<2>处调用协程的close()函数,关闭协程,再调用send()方法,会发现抛出StopIteration异常
当发送给协程不是数字,会导致协程内部有异常抛出
for i in range(1, 6):
try:
print(coro_avg.send(i))
if i % 3 == 0:
coro_avg.send('')
except StopIteration:
print("协程已结束")
except TypeError:
print("传入值异常")
运行结果:
1.0
1.5
2.0
传入值异常
协程已结束
协程已结束
我们设置,当i为3的时候,多传入一个空字符串,结果协程抛出类型错误,协程将运行状态改为结束,之后再往协程传值,都抛出StopIteration异常
我们可以让协程处理一些特定的异常,比如:
class DemoException(Exception): # <1>
pass def demo_exec_handling():
print("coroutine started")
while True:
try:
x = yield # <2>
except DemoException: # <3>
print("DemoException handled")
else:
print("coroutine received:{}".format(x))
运行结果
>>> exec_coro = demo_exec_handling()
>>> next(exec_coro)
coroutine started
>>> print(exec_coro.send(1))
coroutine received:1
None
>>> exec_coro.send(2)
coroutine received:2
>>> exec_coro.send(3)
coroutine received:3
>>> exec_coro.throw(DemoException)
DemoException handled
>>> exec_coro.send(4)
coroutine received:4
>>> exec_coro.throw(ZeroDivisionError)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in demo_exec_handling
ZeroDivisionError
>>> exec_coro.send(5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
<1>处的DemoException是用来测试协程的,首先我们可以看到,当yield右边没有任何式子时,返回给调用方的是一个None对象,其次如果我们调用throw()方法将异常传入协程,因为协程里有关于DemoException的捕捉,所以协程会继续执行,当我们继续传入ZeroDivisionError,则协程结束
让协程返回值
我们可以改造之前的averager()函数,使它可以返回一个对象,对象里有count和average两个属性
from collections import namedtuple Result = namedtuple("Result", ["count", "average"]) def averager():
total = .0
count = 0
average = None
while True:
term = yield average
if term is None:
break
total += term
count += 1
average = total / count
return Result(count, average)
运行结果: >>> coro_avg = averager()
>>> next(coro_avg)
>>> coro_avg.send(10)
10.0
>>> coro_avg.send(20)
15.0
>>> coro_avg.send(30)
20.0
>>> coro_avg.send(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: Result(count=3, average=20.0)
当我们发送None的时候,协程结束,返回结果,一如既往,生成器会抛出StopIteration异常,异常对象的value属性保存着返回值,为了获取返回值,我们还要再修改一下代码
try:
coro_avg = averager()
next(coro_avg)
coro_avg.send(10)
coro_avg.send(20)
coro_avg.send(30)
coro_avg.send(None)
except StopIteration as exc:
result = exc.value
print(result)
运行结果:
Result(count=3, average=20.0)
结语:关于协程yield结构这一块,到此暂做结束,下一章会介绍协程的yield from结构,yield from结构会在内部自动捕获StopIteration异常,还会把协程的返回值变成yield from表达式的值,下一章节将会讨论yield from的结构和用法,谢谢大家
Python协程详解(一)的更多相关文章
- python协程详解
目录 python协程详解 一.什么是协程 二.了解协程的过程 1.yield工作原理 2.预激协程的装饰器 3.终止协程和异常处理 4.让协程返回值 5.yield from的使用 6.yield ...
- python协程详解,gevent asyncio
python协程详解,gevent asyncio 新建模板小书匠 #协程的概念 #模块操作协程 # gevent 扩展模块 # asyncio 内置模块 # 基础的语法 1.生成器实现切换 [1] ...
- Python协程详解(二)
上一章,我们介绍了Python的协程,并讲到用yield达到协程的效果,这一章,我们来介绍yield from的结构和作用 我们先来对比下yield和yield from的用法 def first_g ...
- Python进程、线程、协程详解
进程与线程的历史 我们都知道计算机是由硬件和软件组成的.硬件中的CPU是计算机的核心,它承担计算机的所有任务. 操作系统是运行在硬件之上的软件,是计算机的管理者,它负责资源的管理和分配.任务的调度. ...
- PHP协程 详解
[开源中国] PHP 使用协同程序实现合作多任务 [风雪之隅] 在PHP中使用协程实现多任务调度
- Golang 之协程详解
转自:https://www.cnblogs.com/liang1101/p/7285955.html 一.Golang 线程和协程的区别 备注:需要区分进程.线程(内核级线程).协程(用户级线程)三 ...
- Python协程与Go协程的区别二
写在前面 世界是复杂的,每一种思想都是为了解决某些现实问题而简化成的模型,想解决就得先面对,面对就需要选择角度,角度决定了模型的质量, 喜欢此UP主汤质看本质的哲学科普,其中简洁又不失细节的介绍了人类 ...
- Python核心技术与实战——十六|Python协程
我们在上一章将生成器的时候最后写了,在Python2中生成器还扮演了一个重要的角色——实现Python的协程.那什么是协程呢? 协程 协程是实现并发编程的一种方式.提到并发,肯很多人都会想到多线程/多 ...
- 《python开发技术详解》|百度网盘免费下载|Python开发入门篇
<python开发技术详解>|百度网盘免费下载|Python开发入门篇 提取码:2sby 内容简介 Python是目前最流行的动态脚本语言之一.本书共27章,由浅入深.全面系统地介绍了利 ...
随机推荐
- JAVA4大线程池
不知不觉中我们电脑的硬件设施越来越好,从双核四线程普及到如今四核八线比比皆是.互联网发展至今,讲究的就是快,less is more,而且大数据的诞生和各种种类繁多的需求处理,单线程的程序逐渐不能满足 ...
- 从零开始的全栈工程师——js篇2.17(属性和节点获取)
DOM 一.节点树状图 Document>documentElement>body>tagname 二.常用的节点类型 元素节点(标签) 文本节点(文本节点) 属性节点(标签里的属性 ...
- https 双向验证
服务器配置 服务器秘钥 服务器公钥证书 ,客户端公钥证书 客户端配置 客户端秘钥+密码 服务器公钥证书 目前android验证ok,pc浏览器添加客户端秘钥证书 ,访问还是失败,待继续查找资 ...
- C#问题记录-CallbackOnCollectedDelegate
做项目的时候遇到了这个问题: 检测到:CallbackOnCollectedDelegate 对“xx.HookProc::Invoke”类型的已垃圾回收委托进行了回调.这可能会导致应用程序崩溃.损坏 ...
- python super用法
普通继承 class FooParent(object): def __init__(self): self.parent = 'I\'m the parent.' print 'Parent' de ...
- shell实现网站备份
#!/bin/bash ##back web directory scripts #需要备份的目录写入与脚本同级目录test.txt文件中 DIR="/data/server/www&quo ...
- 从asp.net到jsp:3分钟看透Jsp&Servlet
零:JSP是谁? 话说故事的开头是这样的:JSP全名为Java Server Pages,其根本是一个简化的Servlet设计:后来的详细事情有请各位自便.美女→找→谷哥 or 帅哥→找→度娘 插播: ...
- Select与SelectMany
SelectMany在MSDN中的解释:将序列的每个元素投影到 IEnumerable(T) 并将结果序列合并为一个序列. 不用去用foreach进行两次遍历,就可以将子循环需要的元素过滤出来... ...
- iphone 弹出键盘,文本框自动向上移动。
1.让类继承UITextViewDelegate UITextView *inputTextView;UIScrollView * _scrollView; 2.在init函数中先创建scrollVi ...
- innobackupex基于binlog日志的恢复 -- 模拟slave恢复
说明:一般来说,如果恢复的binlog量不大,可以使用此方法来恢复:mysqlbinlog /data/mysqlbak/binlogbak/restoredb-bin.000018 |mysql - ...