写在前面
从底层到第三方库,全面讲解python的异步编程。这节讲述的是python异步编程的底层原理第一节,详细了解需要配合下一节观看哦。纯干货,无概念,代码实例讲解。
本系列有6章左右,点击头像或者专栏查看更多内容,陆续更新,欢迎关注。
部分资料来源及参考链接:
https://www.bilibili.com/video/BV1Li4y1j7RY/
https://zh.wikipedia.org/wiki/%E5%8D%8F%E7%A8%8B
同步与异步
同步必须顺序执行,异步可以大家一起执行。但是同步由于是顺序执行,结果是有序的,异步的结构是无序的。
迭代与遍历
迭代常用于数组,列表,元组等有序结构;遍历则常用于二叉树,字典等无序结构。
为什么是这样呢?
这是因为迭代有一个next(见《数据结构》),这个next是有指向性的,但是遍历就是无序搜索
重点:
- 迭代比遍历要快
- 遍历可以强行用在数组,列表等有序结构,但迭代不能搜索无序结构
- (这就是为啥很多老师喜欢把迭代叫成遍历,这是一种误导初学者)
迭代器(iter)
你可以把任何有序的类型,转换为可迭代对象,就像这样
list_data = [0,1,2,3,4,5,6,7,8,9]
iterator_data = iter(list_data)#将可迭代对象 转成 迭代器
print(type(iterator_data))
你也可以使用next方法了,
print(type(iterator_data))
那么,如何创建迭代器呢?
迭代器具有特殊的__iter__,__next__
方法
class Numbers:
def __init__(self):
self.a = 1
def __iter__(self):#必须返回自己
return self
def __next__(self):
data = self.a
self.a+= 1
return data
#'__iter__和__next__同时出现就是迭代器了,普通类并没有这两个方法'
NumBer = Numbers()
print(next(NumBer))
print(next(NumBer))
print(next(NumBer))
iter方法写法是固定的,next魔法方法会在使用next时自动调用。
生成器(generator)
相比迭代器,它多了一个yield,而这个关键字可以实现函数状态的挂起与恢复。用以下的代码来体会一下这个含义
def get_data():
list_data = [0,1,2,3,4,5,6,7,8,9]
for data in list_data:
return data #直接就终止了函数
#yield data #直接挂起了函数,等待下次恢复
generator_data = get_data()
print(generator_data)
print(type(get_data()))
在return的状态下,马上就会返回并退出函数。但是如何得到我们想要的结果呢,每一次都返回一个迭代出来的值,这就是yield,改成下面的代码:
def get_data():
list_data = [0,1,2,3,4,5,6,7,8,9]
for data in list_data:
#return data #直接就终止了函数
yield data #直接挂起了函数,等待下次恢复
generator_data = get_data()
print(generator_data)
print(type(get_data()))
输出结果为
返回了一个生成器,它是可以使用next方法来获取值的,执行print(next(generator_data))
就输出了一个0,再次执行,就会一个个输出。
当然你也可以使用for来进行输出。
所以,其实这里就是yield与循环的巧妙应用。工作流程就像这样:
1. 方法中携带了yield关键字,首次调用时,会返回generator生成器。
2. 首次使用next,生成器激活,开始执行并在yield关键字位置返回,同时挂起当前函数状态。
3. 再次使用next,保存了之前的状态,从yield的下一行开始执行。
协程(coroutine)
协程是一个非常重要的概念。对于理解后续golang中的协程goroutine底层原理有很大帮助。这里先解释coroutine
对于协程,wiki百科有这样的信息
协程(英语:coroutine)是计算机程序的一类组件,推广了协作式多任务的子例程,允许执行被挂起与被恢复。
生成器,也叫作“半协程”,是协程的子集。尽管二者都可以yield多次,挂起(suspend)自身的执行,并允许在多个入口点重新进入,但它们特别差异在于,协程有能力控制在它让位之后哪个协程立即接续它来执行,而生成器不能,它只能把控制权转交给调用生成器的调用者。在生成器中的yield语句不指定要跳转到的协程,而是向父例程传递返回值。
(上述信息的参考链接如下)
https://zh.wikipedia.org/wiki/%E5%8D%8F%E7%A8%8B
通过上述信息,我们可以知道:协程是允许被挂起与被恢复的;协程是可以通过生成器实现的。再回到生成器的next
方法,它做了什么呢?
next
官方文档是这样说的
generator.next()
开始一个生成器函数的执行或是从上次执行 yield 表达式的位置恢复执行。 当一个生成器函数通过 next() 方法恢复执行时,当前的 yield 表达式总是取值为 None。 随后会继续执行到下一个 yield 表达式,这时生成器将再次挂起,而 expression_list 的值会被返回给 next() 的调用方。 如果生成器没有产生下一个值就退出,则将引发 StopIteration 异常。
此方法通常是隐式地调用,例如通过 for 循环或是内置的 next() 函数。
(上述信息的参考链接如下)
https://docs.python.org/zh-cn/3/reference/expressions.html?highlight=send#generator.send
现在来调试一段代码,体会一下吧
此时没有输出。data是一个被挂起的生成器。这就说明,yield可以类似于return一样,终止函数运行。
接下来调用next方法
进入函数内部,同时指针指向‘挂起了’,yield即刻返回,没有给a赋值,也没有执行函数中的print语句
继续执行,再次跳入函数
此时再次恢复函数,执行print语句,但是a仍旧没有赋值
这里,没有产生下一个值,所以返回了stopiteration
现在,再倒回去看官方文档的解释就非常清楚了。
开始一个生成器函数的执行或是从上次执行 yield 表达式的位置恢复执行。 当一个生成器函数通过 next() 方法恢复执行时,当前的 yield 表达式总是取值为 None。 随后会继续执行到下一个 yield 表达式,这时生成器将再次挂起,而 expression_list 的值会被返回给 next() 的调用方。 如果生成器没有产生下一个值就退出,则将引发 StopIteration 异常。
send
还需要了解一个重要的send方法,其实就是前文中有一句 当一个生成器函数通过 __next__() 方法恢复执行时,当前的 yield 表达式总是取值为 None
,而send方法可以更改这个取值的结果,就像这样
此时的程序结果就是
这里下面当然是报出了StopIteration错误,因为和next一样的,没有下一个值了。
官方文档解释:
恢复执行并向生成器函数“发送”一个值。 value 参数将成为当前 yield 表达式的结果。 send() 方法会返回生成器所产生的下一个值,或者如果生成器没有产生下一个值就退出则会引发 StopIteration。 当调用 send() 来启动生成器时,它必须以 None 作为调用参数,因为这时没有可以接收值的 yield 表达式。
捕获协程异常的值
对于生成器,它可以及时捕获异常值,用于检视多次恢复中可能产生的异常。
看看下面的代码
def get_data(data):
a = yield data
if a is None: #此处的None为结尾,直接抛出异常,结束生成器
print('第一次send:恢复执行')
return '结束了'
print('send的值:{}'.format(a))
data = get_data('挂起了')
print('第一次next:{}'.format(next(data))) #必须调用next() 才能用send()
try:
data.send(None) #结束生成器
except StopIteration as e:
print('抛出异常:{}'.format(e.value))
执行结果:
简单解释一下这个代码,第一行结果就是首次激活,在yield挂起,第二行由于send None,进入if语句分支,print了恢复执行
。
然后有一个return语句,不知道return到了哪里。然后就会抛出StopIteration
异常,因为send没有得到下一个值,此时弹出进入except分支,通过一个.value
属性拿到了return的值
关键就在这里了,为什么这个异常,有一个value属性呢?
官方文档是这样说的:
exception StopIteration
由内置函数 next() 和 iterator 的 next() 方法所引发,用来表示该迭代器不能产生下一项。value 该异常对象只有一个属性 value,它在构造该异常时作为参数给出,默认值为 None。
当一个 generator 或 coroutine 函数返回时,将引发一个新的 StopIteration实例,函数返回的值将被用作异常构造器的 value 形参。
上述资料链接:
https://docs.python.org/zh-cn/3/library/exceptions.html#StopIteration
简单来说就是,由于你有一个return,表明当前生成器或协程是必须结束的状态,不可能会得到下一个next值,此时会产生一个新的StopIteration实例,并把返回值填充至它的value属性中。
yield from
官网链接
https://docs.python.org/zh-cn/3/whatsnew/3.3.html#pep-380
官方文档是这样说的:
允许生成器将其部分操作委托给另一个生成器。这允许包含的一段代码被分解出来并放置在另一个生成器中。此外,允许子生成器返回一个值,并且该值可供委托生成器使用。
但是,与普通循环不同,它允许子生成器直接从调用范围接收发送和抛出的值,并将最终值返回到外部生成器
简单来说就是,yield from 和 for循环很类似,但yield from功能更强大,yield from 还能自动捕获StopIteration异常,并输出异常对象的value属性。
看下面这段代码
def accumulate():
tally = 0
while 1:
next = yield
if next is None:
return tally
tally += next
def gather_tallies(tallies):
while 1:
tally = yield from accumulate()
tallies.append(tally)
tallies = []
acc = gather_tallies(tallies)
next(acc) # Ensure the accumulator is ready to accept values
for i in range(4):
acc.send(i)
acc.send(None) # Finish the first tally
for i in range(5):
acc.send(i)
acc.send(None) # Finish the second tally
tallies
[6, 10]
这是官方的代码,这里就可以看出来。一旦输入的值发生了异常,我们就可以return退出生成器,同时利用yield from进行捕获,不需要再写try-except,和呼出value属性。
协程新写法
了解了半协程
,来看看真协程是怎么写的。协程是一个函数,只是它满足以下特点:
1. 有 IO 依赖的操作
2. 可以在进行 IO 操作时暂停
3. 无法直接执行
第一点就是输入输出流,就是前面提到的send方法,yield from。第二点就是可以进行挂起。第三点表示要使用next来进行迭代,不能直接执行。
它的作用就是对有大量 IO 操作的程序进行加速
Python 协程属于 可等待 对象
因此可以在其他协程中被等待
注意:
- 协程是单线程的,只是控制自己,而多线程是切换线程,切换线程需要不少资源
- 协程可以和多线程一起用,达到最高效率
在3.5版本之前是这样的:
@asyncio.coroutine
def old_data():#旧写法
print('正在执行')
yield from asyncio.sleep(2)#沉睡2秒
print('执行完毕')
此时将asyncio作为装饰器使用,使用了yield from来接受错误信息
asyncio
接上面,现在新版的写法是这样
import asyncio
async def new_data():
print('正在执行')
await asyncio.sleep(2)#沉睡2秒
print('执行完毕')
asyncio.run(new_data())#运行新写法
@asyncio.coroutine 替换为 async
yield from 替换为 await