python-复盘-从yield/send到yield from再到async/await

Python中的协程大概经历了如下三个阶段:

  1. 最初的生成器变形yield/send
  2. 引入@asyncio.coroutine和yield from
  3. 在最近的Python3.5版本中引入async/await关键字

一、生成器变形yield/send

In [31]: def gen():
    ...:     value = 888168
    ...:     while 1:
    ...:         rece = yield value
    ...:         if rece == 'e':
    ...:             break
    ...:         value = 'got: %s' % rece
    ...:

In [32]: g = gen()

In [33]: g.send(None)    # 如何理解第一次的None?
Out[33]: 888168

In [34]: g.send('hello')
Out[34]: 'got: hello'

In [35]: g.send('e')
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-35-422bc441ae72> in <module>()
----> 1 g.send('e')

StopIteration:

这个协程很简单,关键是理解第一步None:
None在计算机中表示 无 的意思,说白了 send(None)并没有发送None,因为None就是啥都无,它的本意是启动这个生成器。好了,如何启动?
第一次启动是按顺序执行 gen() 的。先是执行value = 888168,然后继续,直到 rece = yield value 的 yield value部分停止。注意是 yield value部分停止。所以send(None)的结果才是888168。yield value 是暂停点,rece = yield是下次的启动点。


二、yield from

In [36]: def g1():
    ...:     yield range(4)    # range 中元素未释放出,你是无法 yield 的

In [37]: def g2():
    ...:     yield from range(4)    # 把 range 中元素释放出来再yield成一个生成器

In [38]: it1 = g1()

In [39]: it2 = g2()

In [40]: for x in it1:     # 某种意思上等同一个生成器套娃,你一次 for in 不行的。
    ...:     print(x)
    ...:
range(0, 4)

In [41]: for x in it2:     # 释放生成器元素成果
    ...:     print(x)
    ...:
0
1
2
3

这说明yield就是将range这个可迭代对象直接返回了。
而yield from解析了range对象,将其中每一个item返回了。
yield from iterable本质上等于for item in iterable: yield item的缩写版

来看一下例子,假设我们已经编写好一个斐波那契数列函数

In [42]: def fab(max):      # 定义一个生成器 
    ...:     n,a,b = 0, 0, 1
    ...:     while n < max:
    ...:         yield b
    ...:         a, b = b, a+ b
    ...:         n = n + 1

In [43]: f = fab(4)

In [44]: f
Out[44]: <generator object fab at 0x1089814c0>
In [45]: def f_wrapper(fun_iterable):
    ...:     print('start')
    ...:     for item in fun_iterable:   # for item 就是把生成器的元素给释放出来了
    ...:         yield item              # 但这里,yield又把释放的元素变成了生成器
    ...:     print('end')

In [46]: wrap = f_wrapper(fab(5))

In [47]: for i in wrap:                 # 所以最后打印时候需要 for i in 来释放元素
    ...:     print(i, end='')
    ...:
start
11235end    # 结果就是1,1,2,3,5
In [48]: import logging

In [49]: def f_wrapper2(fun_iterable):
    ...:     print('start')
    ...:     yield from fun_iterable    # 等同上方代码,把元素释放出来,重新yield后再组成新生成器
    ...:     print('end')

In [50]: wrap = f_wrapper2( fab(5) )

In [52]: for i in wrap:                 # 释放元素
    ...:     print(i, end='')
    ...:
start
11235end

yield from 后面必须跟iterable对象(可以是生成器,迭代器)


三、asyncio.coroutine和yield from

yield from在asyncio模块中得以发扬光大。之前都是我们手工切换协程,现在当声明函数为协程后,我们通过事件循环来调度协程。

先看示例代码

import asyncio,random
@asyncio.coroutine
def smart_fib(n):
    index = 0
    a = 0
    b = 1
    while index < n:
        sleep_secs = random.uniform(0, 0.2)
        yield from asyncio.sleep(sleep_secs) #通常yield from后都是接的耗时操作
        print('Smart one think {} secs to get {}'.format(sleep_secs, b))
        a, b = b, a + b
        index += 1

@asyncio.coroutine
def stupid_fib(n):
    index = 0
    a = 0
    b = 1
    while index < n:
        sleep_secs = random.uniform(0, 0.4)
        yield from asyncio.sleep(sleep_secs) #通常yield from后都是接的耗时操作
        print('Stupid one think {} secs to get {}'.format(sleep_secs, b))
        a, b = b, a + b
        index += 1

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    tasks = [
        smart_fib(10),
        stupid_fib(10),
    ]
    loop.run_until_complete(asyncio.wait(tasks))
    print('All fib finished.')
    loop.close()

结果:

stupid one think 0.16154263754880197 secs to get 1   # stu花费0.16
stupid one think 0.05444056751627722 secs to get 1   #  stu花费0.05 + 0.16 = 0.21
stupid one think 0.0021981642884256304 secs to get 2  # stu花费0.21 + 0.002 = 0.212 
smart one think 0.37663551871717216 secs to get 1  # sma花费 0.376,随机生成的0.37>0.21,所以smart才在这时候开始执行
smart one think 0.2645472221109239 secs to get 1   # 这就是异步高效的秘密,谁快先执行
smart one think 0.15856033283994533 secs to get 2
stupid one think 0.6443246613879796 secs to get 3
stupid one think 0.02350589883775811 secs to get 5
smart one think 0.1801088550985621 secs to get 3
smart one think 0.3576929865189507 secs to get 5
stupid one think 0.569321733443974 secs to get 8
stupid one think 0.008074465892759975 secs to get 13
smart one think 0.33219791971546947 secs to get 8
stupid one think 0.23956673657999528 secs to get 21
stupid one think 0.011677097683787352 secs to get 34
stupid one think 0.06350886018228774 secs to get 55
smart one think 0.13317102581385262 secs to get 13
smart one think 0.21706707561261382 secs to get 21
smart one think 0.2992267724097271 secs to get 34
smart one think 0.24725642731182954 secs to get 55
all fib finished

yield from语法可以让我们方便地调用另一个generator。
本例中yield from后面接的asyncio.sleep()是一个coroutine(里面也用了yield from),所以线程不会等待asyncio.sleep(),而是直接中断并执行下一个消息循环。当asyncio.sleep()返回时,线程就可以从yield from拿到返回值(此处是None),然后接着执行下一行语句。
asyncio是一个基于事件循环的实现异步I/O的模块。通过yield from,我们可以将协程asyncio.sleep的控制权交给事件循环,然后挂起当前协程;之后,由事件循环决定何时唤醒asyncio.sleep,接着向后执行代码。
协程之间的调度都是由事件循环决定。
yield from asyncio.sleep(sleep_secs)这里不能用time.sleep(1)因为time.sleep()返回的是None,它不是iterable,还记得前面说的yield from后面必须跟iterable对象(可以是生成器,迭代器)
所以会报错:

yield from time.sleep(sleep_secs) 
TypeError: ‘NoneType’ object is not iterable


四、async和await

弄清楚了asyncio.coroutineyield from之后,在Python3.5中引入的asyncawait就不难理解了:可以将他们理解成asyncio.coroutine/yield from的完美替身。当然,从Python设计的角度来说,async/await让协程表面上独立于生成器而存在,将细节都隐藏于asyncio模块之下,语法更清晰明了。
加入新的关键字 async ,可以将任何一个普通函数变成协程

import time,asyncio,random
async def mygen(alist):
    while len(alist) > 0:
        c = randint(0, len(alist)-1)
        print(alist.pop(c))
a = ["aa","bb","cc"]
c=mygen(a)
print(c)
输出:                   # 只是协程,不是异步,异步必须有 await
<coroutine object mygen at 0x02C6BED0>

在上面程序中,我们在前面加上async,该函数就变成一个协程了。

但是async对生成器是无效的。async无法将一个生成器转换成协程。
还是刚才那段代码,我们把print改成yield

async def mygen(alist):
    while len(alist) > 0:
        c = randint(0, len(alist)-1)
        yield alist.pop(c)    # 单独的 async 与生成器是无法变成协程的,async 遇到生成器无法把函数变成协程
a = ["ss","dd","gg"]
c=mygen(a)
print(c)

可以看到输出

<async_generator object mygen at 0x02AA7170>

并不是coroutine 协程对象

所以我们的协程代码应该是这样的

import time,asyncio,random
async def mygen(alist):
    while len(alist) > 0:
        c = random.randint(0, len(alist)-1)
        print(alist.pop(c))
        await asyncio.sleep(1)     # 非生成器函数,配合 asynic 成为协程,再配合 await 成为异步
strlist = ["ss","dd","gg"]
intlist=[1,2,5,6]
c1=mygen(strlist)
c2=mygen(intlist)
print(c1)

要运行协程,要用事件循环
在上面的代码下面加上:

if __name__ == '__main__':
        loop = asyncio.get_event_loop()
        tasks = [                        # 事件循环
        c1,
        c2
        ]
        loop.run_until_complete(asyncio.wait(tasks))
        print('All fib finished.')
        loop.close()

就可以看到为追求高效率的交替执行的效果。



Python黑魔法 --- 异步IO( asyncio) 协程

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容