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章,由浅入深.全面系统地介绍了利 ...
随机推荐
- Teradata 认证系列 - 1. TCPP这是个啥
一看历史,好几年没发帖...正好最近在自学teradata认证(学也不一定学的完,最后也不一定去考,仅仅安慰一下不想碌碌无为的内心) 网上一搜,百度上的中文相关资料简直为0.这个不奇怪,毕竟都没什么人 ...
- swift 2特性记录
swift 团队一直在优化,让大家准备在秋天的时候,迁移到swift2做准备. 一.错误处理 异常处理,不是NSError对象和双指针. 可以使用 throws 来指定方法来抛出一个错误. 调用d ...
- Educational Codeforces Round 51 (Rated for Div. 2)
做了四个题.. A. Vasya And Password 直接特判即可,,为啥泥萌都说难写,,,, 这个子串实际上是忽悠人的,因为每次改一个字符就可以 我靠我居然被hack了???? %……& ...
- 【php】
1 <?php $arr = [ 'a' => 'aaa', ]; $arr2 = $arr; $arr2['a'] = 'ccc'; print_r($arr); print_r($ar ...
- n宫格的实现方法
方法一.table 1.看成多列 <style> .line{ display: table; width: 1024px; clear:both; overflow:auto; /*-- ...
- WPF学习一:XAML的资源(Resources)结构
一个初学者,把知识做个积累,如果有不对的地方,还请高手指出,谢谢! 先看一段代码:(下面是以Window WPF进行讲解,如果是Web 的话就把<Window改为<Page 而如果是 Us ...
- WIN7 64位对Excel操作异常
在本地做Excel导出功能的测试时,报出“检索COM 类工厂中CLSID 为 {00024500-0000-0000-C000-000000000046}的组件时失败”的异常,知道要对Excel进行D ...
- 流媒体 6——MPEG电视
1.电视图像的数据率 1.1 ITU-R BT.601标准数据率 按照奈奎斯特(Nyquist)采样理论,模拟电视信号经过采样(把连续的时间信号变成离散的时间信号)和量化 (把连续的幅度变成离散的幅度 ...
- Java分代的思考
我们都知道,java的gc是基于java的分代前提,不管是CMS还是G1,都是基于分代思想:老年代和新生代 思考一:为什么可以分代? 1.java中对象的生命周期是不一样的,在gc中对应年龄的概念. ...
- Springboot端口设置
application.properties 加入 server.port=80