python——理一理yield和yield from、coroutine和生成器、async/await

总之就是黑人问号.gif。
跟着廖大神学python就是等到实战部分的时候就发现自己啥也没搞清楚,实战第三天,我已经黑了两天。

python——理一理yield和yield from、coroutine和生成器、async/await_第1张图片

文章目录

      • 1、前言
      • 2、生成器——yield其实是个传送门
        • 2.1、使用yield传送输出值
        • 2.2、使用yield辅助定点投放输入值
        • 2.3、小结
      • 3、协程
        • 3.1、协程的工作模式
        • 3.2、asyncio
      • 4、yield和yield from
      • 5、异步
        • 5.1、async_generator异步生成器
        • 5.2、await
      • 6、总结

关于yield和async/await的几个特别好的参考:
python中yield的用法详解——最简单,最清晰的解释
async/await入门指南

1、前言

Python教程实战第三天的部分,编写ORM,本来觉得对于生成器和异步IO的理解还挺清楚的,结果等写不下去只能抄代码的时候发现各种yield和yield from还有async/await交替使用的时候果然就一脸懵了(哼!幼齿的感觉真好!)总之,看了一会之后我的内心的疑惑已经简化成了“咦,yield跟from为啥要放到一起丫,为啥这个函数里的for循环前面也要用async……”这样的循环bgm
哦,然后就是从头开始撸。yield它爸是生成器,yield from它爸是……啥?

2、生成器——yield其实是个传送门

关键字不是用来定义一种规则的么。yield关键字定义了一种函数执行的规则,和return不一样的规则,所以使用yield的函数就叫做不一样函数(啥,是生成器啊啊啊)。

第一次见到yield就是在生成器里。
复习一下生成器的两种创建方法:

  • 将一个列表生成式的[]改成()
 L = [x * x for x in range(10)]   # 列表
 g = (x * x for x in range(10))   # 生成器
  • 包含yield关键字:其实应该说是用yield来返回生成结果
# 斐波拉契数列生成器
def fib(max):
   n, a, b = 0, 0, 1
   while n < max:
       yield b
       a, b = b, a + b
       n = n + 1
   return 'done'

2.1、使用yield传送输出值

使用yield的函数,执行流程就和普通函数不一样了:
普通函数:遇到return或者最后一行函数语句就返回。
generator:每次调用next()执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行
不一样函数生成器和一般函数不一样的地方在于(它很特别):

  • 特别的关键字yield:使用yield传送输出值
  • 特别的打开方式next()方法:直接调用函数并不会执行,需要调用next方法来执行
  • 特别懒:调用一次执行一次规则计算(看到yield就不干活了,把值给调用方一丢——交作业,瘫着爱咋咋)
    不过生成器不就是用来节省空间的么,本来就是打算让它边循环边计算的(基因懒,很优秀)。
def odd():
    print('step 1')
    yield 1
    print('step 2')
    yield(3)
    print('step 3')
    yield(5)
    return 'Done'

o = odd()
print(next(o))
print(next(o))
print(next(o))
print(next(o))

输出:
python——理一理yield和yield from、coroutine和生成器、async/await_第2张图片
不知道为啥看这一类包含多个yield的函数的时候会觉得恶意满满,前面还觉得yield是只是放学铃,这一看,yield还是下课铃,一出现就可以休息(学生狗真幸福!)
当然,如果游戏已经结束了(return了或者没有可执行的代码)却一直不给下课铃(上面例子种最后一个next()调用,generator会继续执行但后面再没有yield),就有人报警了(StopIteration),然后报警完了还会带出游戏结束时的返回值(return 'Done')?
补充一下要通过正规途径获取游戏结束的返回值,就是自己多看着点小调皮们,逮住那个出去告状的小可怜:
(以下是坏坏的班主任上线的代码)

def odd():
    print('step 1')
    yield 1
    print('step 2')
    yield 3
    print('step 3')
    yield 5
    return 'Done'

o = odd()
while True:
	try:
		print(next(o))
	except StopIteration  as e:
		print('return %s' % e.value)
		break;

输出:
python——理一理yield和yield from、coroutine和生成器、async/await_第3张图片

2.2、使用yield辅助定点投放输入值

虽然掌握了计算方法,但是只是闷头自习可是没办法与时俱进的(bgm~红极一时?的大武汉口号。快感受下那个一身正气的感叹号)

python——理一理yield和yield from、coroutine和生成器、async/await_第4张图片

还需要不定期地接受点新鲜的输入信息。
next函数只是推一把generator让它动起来,充其量是个课代表,负责督促大家写作业,然后收收作业啥的。send就不一样了,是任课老师捏,还会给新的东西布置作业(传入参数)。sned方法种包含next方法。和next调用的执行流程一样,再次调用时还是从上次结束的yield开始,只不过还会传入参数,传入的参数等效于yield语句的返回值(假装是根据上次作业的完成情况调整作业量?)

def odd():
    ret = yield 1
    print('step %s' % ret)

    ret = yield 3
    print('step %s' % ret)

    ret = yield 5
    print('step %s' % ret)
    return 'Done'

o = odd()
# 因为send是要一开始去生成器上次停留的yield哪里投放参数的,所以先要让生成器跑到一个yield处,有两种方式:
# 1、调用next()
# 2、调用参数为None的send函数
print(o.send(None))
step = 1
while True:
	try:
		print(o.send(step))
		step = step + 1
	except StopIteration  as e:
		print('return %s' % e.value)
		break;

输出
python——理一理yield和yield from、coroutine和生成器、async/await_第5张图片
最需要说的地方大概就是程序里的那个注释,就酱~放学
(所以说yield是个传送门没错吧!这大概就是我目前对于yield的全部认识)

2.3、小结

其实回到最开始遇见yield的地方,你会发现“人生若只如初见……”也挺没意思的,至于后来yield为啥会勾搭上from……(其实他俩根本没啥关系好伐,就是因为总是让人误会,所以给yield from改名字叫做await)总之就是兜一圈发现,yield还是那个yield,喊generator叫爸?

3、协程

突然有一天yield又出来搞事情了……事情变得复杂可能是因为人心多变( - 报告,有人想偷懒! - 啥?哇哦~太棒了!)

小升初,作业变难变多写不完了,没得办法,只好长出三头六臂(喊三姑六姨家的小可爱们帮忙)写作业,但是……编不下去了,摔!(′д` )…彡…彡
Python因为有GIL,多线程都是假的多线程,大部分情况下都是用多进程实现并发。
然鹅让三姑六姨家帮忙写,有一点麻烦就是大家都不住在一起,作业拆分、合并、互相通知完成情况,想想就头大丫(头发本来就少,为什么还要操心那么多,哭!)。
终于,因为总是要熬夜写作业,爸妈看不下去了,提出了一个方案:协程
(编故事超累,所以你看作业是不是超难写,此处应该给开明的爸妈鼓掌,送小红花)

3.1、协程的工作模式

还是前面的知乎大神写的关于协程的来源的文章:从0到1,Python异步编程的演进之路
当然不是像我这样胡扯瞎编的,人家故事的主人公可不是小学生!

协程看上去其实跟线程工作模式差不多,就是可以在程序内部中断,然后转而执行别的程序程序,在适当的时候,再返回来接着执行。
差不多就是,小红负责写所有科目作业的前10道和最后10道题目,爸妈负责写其他的。小红做完语文前10题,就把作业交给爸妈,然后继续做数学的前10题。“同时”,爸妈做最后10道题目之前的部分,爸妈做完之后再把语文作业给小红,小红继续做剩下的部分……
母慈子孝,其乐融融

协程的优势:

  • 同一线程内,实际上是子程序的切换而不是线程的切换,没有线程切换的开销
  • 共享资源不必加锁,只需要判断状态,不存在同时写变量的冲突,效率高

来一个典型的生产着消费者的栗子(凑字数时间到!)

def consumer():
	r = ''
	while True:
		n = yield r
		print('----r:%s ---- n:%s' % (r,n))
		if not n:
			return
		print('[CONSUMER] Consuming %s...' % n)
		r = '200 OK'

def produce(c):
	c.send(None)
	n = 0
	while n < 3:
		n = n + 1
		print('[PRODUCER] Producing %s...' % n)
		r = c.send(n)
		print('[PRODUCER] Consumer return: %s' % r)
	c.close()

c = consumer()
produce(c)

#输出
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[Finished in 0.6s]

这就是协程!是真皮,不是人造革!
注意,此时的yield还是那个yield。

这下,小红终于开开心心地上小学啦~其他同学和家长纷纷效仿。一时间,到超市买碗的人都少了(一起写作业之后家长发现原来笨不是孩子的错,是自己的,再也没有在家摔碗啦)
学校一看,反思了原来布置作业的模式,认为确实有必要布置需要家长参与的作业,同时又担心不当的操作会导致学生成养成不好的习惯。于是开会讨论,结合当前实际情况给出了一套神器asyncio……

3.2、asyncio

asyncio是python3.4种新增的模块,提供了一种机制,使得可以用协程、IO复用在单线程环境种编写并发模型。
asyncio的编程模型是一个消息循环,将需要执行的协程扔到EventLoop中执行。
划重点就是,把协程丢到EventLoop中。asyncio认识的协程使用装饰器@asyncio.coroutine标记的,然后在协程中调用generator的方式是使用yield from
举栗子

import asyncio

@asyncio.coroutine
def hello():
    print("Hello world!")
    # 异步调用asyncio.sleep(1):
    r = yield from asyncio.sleep(1)
    print("Hello again!")

# 获取EventLoop:
loop = asyncio.get_event_loop()
# 执行coroutine
loop.run_until_complete(hello())
loop.close()

有了这个asyncio,实现协程来做作业更加方便了,只需要按照规定登记一下coroutine(装饰器@asyncio.coroutine),然后使用yield from来切换coroutine做作业。这样,学校布置需要使用协程完成的作业更加方便,家长参与写作业更加简单。(同时学校也知道有多少coroutine来帮你写作业?)

为了不引起误会?python3.5引入了新的语法来表示,那就是async/await:

  • 把@asyncio.coroutine替换为async;
  • 把yield from替换为await。

上面的栗子变成了下面(碳烤味儿)的栗子:

async def hello():
    print("Hello world!")
    r = await asyncio.sleep(1)
    print("Hello again!")

yield和yield from的恩仇在后面会详述。
至此,事情差不多就是这么个事情,但是嘛~世界和平的话哪里来的人类文明的进步呢!

“小明他爸,我觉得yield挺好的,async定义的函数里面就用yield呗……爱咋咋用,心里门儿清就可以”
“小红她妈,你说我是coroutine还是generator丫……是妈”
“小汪,我那天看到班主任悄咪咪用async for … in …,好像有猫腻!……喵?原来你是这样的汪!”
……
学校决定来一场别开生面(背着学生的面,专门和家长讨论)的技术分享大会!

4、yield和yield from

之前为了避免混淆,在介绍asyncio的用法的时候,直接说新的版本中使用await来替换yield from。可以说明两点:

  • yield from的作用使用await这个单词更贴近
  • 并没有否定yield from的用处丫,就是名字没取好而已

在查资料的时候看到一个简单粗暴的解释:yield from iterable本质上等于for item in iterable: yield item的缩写版。把自己当成计算机来的解释程序的话,差不多就是这个样纸滴。下面两种写法是等效的:

  • for循环来实现
def gen():
	for c in 'AB':
		yield c
	for i in range(1,3):
		yield i

l = list(gen())
print(l)

# 输出
['A', 'B', 1, 2]
  • yield from实现
def gen():
	yield from 'AB'
	yield from range(1,3)
l = list(gen())
print(l)

#输出
['A', 'B', 1, 2]

简单来说就是yield后面是一个循环,但是,这个循环不简单,是iterable的,最重要的是循环里面还可以有个yield等着你,这下事情就变得好玩啦。我觉得这个求平均值的例子很好:

# 子生成器
def average_gen():
    total = 0
    count = 0
    average = 0
    while True:
        new_num = yield average
        if new_num is None:
            break
        count += 1
        total += new_num
        average = total/count

    # 每一次return,都意味着当前协程结束。
    return total,count,average

# 委托生成器
def proxy_gen():
    while True:
        # 只有子生成器要结束(return)了,yield from左边的变量才会被赋值,后面的代码才会执行。
        total, count, average = yield from average_gen()
        print("计算完毕!!\n总共传入 {} 个数值, 总和:{},平均数:{}".format(count, total, average))

# 调用方
def main():
    calc_average = proxy_gen()
    next(calc_average)            # 预激协程
    print(calc_average.send(10))  # 打印:10.0
    print(calc_average.send(20))  # 打印:15.0
    print(calc_average.send(30))  # 打印:20.0
    calc_average.send(None)      # 结束协程
    # 如果此处再调用calc_average.send(10),由于上一协程已经结束,将重开一协程

if __name__ == '__main__':
    main()

开始变得有意思了吧,回顾下年轻时候send和yield用法,传送门还是那个传送门,send还是那个send,会从上次传送的位置(生成器里的yield)继续执行。yield from后面带一个生成器的这个做法,把洪荒之力突然就引流到一个神秘的地方了这样用法有专门术语来描述:

  • 委派生成器:包含 yield from 表达式的生成器函数;
  • 子生成器:从 yield from 表达式中 部分获取的生成器;
  • 调用方:调用委派生成器的客户端代码;

这样做的好处是:把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。有了这个结构,协程可以通过以前不可能的方式委托职责。
(上面这句话是抄的,主要就是觉得很厉害)
还有一点yield from不仅仅是做一个循环的地方就是那个看上去很普通的return,能够普普通通就返回了一堆计算结果给委派生成器,是因为yield from在背后做了很多异常处理(有多少我也没看,给出的链接里有伪代码,没有碰到我是不会看的!)

终于有了一丝丝舒畅的感觉,再也不会觉得yield from很奇怪了,这么厉害当然是说什么都对啊!

python——理一理yield和yield from、coroutine和生成器、async/await_第6张图片

咦,突然觉得把yield from改名await不够霸气呢~

此部分的参考链接:
理解Python协程:从yield/send到yield from再到async/await
python协程–yield和yield from
Python并发编程之深入理解yield from语法(八)

提问:为啥此部分没有乱入小学生?
……编不下去啊!!!啊喂,明明是初中生!

不知道有没有人和我一样,在那个求平均值的例子中偷偷摸摸加上一行代码:

print(type(proxy_gen()))

# 输出

是的,普通函数里使用yield from也是个生成器。

5、异步

上面yield和yield from中给出的例子都没有用异步都还是没有涉及到异步的,因为是否异步,yield和yield from都是那么个用法。python在3.5的时候引入语法糖async来实现异步。一个特清楚的demo:

def function():
	return 1

def generator():
	yield 1

async def async_function():
	return 1

async def async_generator():
	yield 1

print(type(function))
print(type(generator()))
print(type(async_function()))
print(type(async_generator()))

# 输出


E:\13_python\async.py:17: RuntimeWarning: coroutine 'async_function' was never awaited
  print(type(async_function()))
RuntimeWarning: Enable tracemalloc to get the object allocation traceback


[Finished in 0.8s]

两个地方要看仔细了:

  • async修饰的普通函数叫做coroutine
  • async修饰的生成器叫做async_generator

5.1、async_generator异步生成器

谣传一时的某家长见过“老师偷偷摸摸”用啥async forasync withasyncawaityield混搭,大概都是在这里整出来的幺蛾子。所以学校最后决定在技术分享大会上使用python3.7。(如你所见,前后并没有什么逻辑关系)
async_generator是在python3.5的时候引进的,关于为何要整一个async iterators,官方的链接在此:The async_generator library
贴出来文中的例子哈。普通生成器读文件:

def load_json_lines(fileobj):
    for line in fileobj:
        yield json.loads(line)

突然有一天,需要从网络读取文件,这个时候就可以用async_generator这样写:

async def load_json_lines(asyncio_stream_reader):
    async for line in asyncio_stream_reader:
        yield json.loads(line)

第一次看到asyncfor放在一起用的时候,下意识觉得是async加匿名函数这样子的,其实不是。还是用之前的例子来看:

async def odd():
    ret = yield 1
    print('step %s' % ret)

    ret = yield 3
    print('step %s' % ret)

    ret = yield 5
    print('step %s' % ret)
    # return 'Done'

for o in odd():
	print(o)

# 输出
Traceback (most recent call last):
  File "E:\13_python\async.py", line 117, in 
    for o in odd():
TypeError: 'async_generator' object is not iterable
[Finished in 0.6s]

明明白白告诉你,async_generator object is not iterable。没有记错,generator是iterable的。
目前所感知的异步生成器async_generator的“异步”大概就是它实现的是个加a的。(好深刻的感知……)也就是它实现的方法是__aiter____anext__,所以迭代的时候用的是async for ... in ...这样的语法啦~
同样的,async with也是因为那个a

第二点乱七八糟的像await yield_这样的用法也是因为时代变迁留下来的,到python3.6,异步生成器的写法基本就是现在看着比较简洁的。详细说明在此

# python3.5
from async_generator import async_generator, yield

@async_generator
async def load_json_lines(stream_reader):
    async for line in stream_reader:
        await yield_(json.loads(line))

# python3.6
async def load_json_lines(stream_reader):
    async for line in stream_reader:
        yield json.loads(line)

不自觉哼起了小曲儿

python——理一理yield和yield from、coroutine和生成器、async/await_第7张图片

5.2、await

了解了yield from的用法,以及后来使用await来替换yield from这个事实,就可以开始愉快地玩await啦。
await比yield from的抽象层高一丢。yield from后面得是一个Iterable,通过循环来理解很方便。但await更强调得是“等”这个操作。在协程函数中,可以通过await来挂起自身的协程,await后面的对象需要是一个Awaitable,一个类实现了__await__方法,它构造的对象就是一个Awaitable。

Coroutine类继承自Awaitable

一个很好的买土豆的例子:

class Potato:
	@classmethod
	def make(cls, num, *args, **kws):
		potatos = []
		for i in range(num):
			potatos.append(cls.__new__(cls, *args, **kws))
		return potatos

all_potatos = Potato.make(5)

async def ask_for_potato():
	await asyncio.sleep(random.random())
	all_potatos.extend(Potato.make(random.randint(1,10)))

async def take_potatos(num):
	count = 0
	while True:
		if len(all_potatos) == 0:
			await ask_for_potato()
		potato = all_potatos.pop()
		yield potato
		count += 1
		if count == num:
			break

async def buy_potatos():
	bucket = []
	for p in take_potatos(20):
		bucket.append(p)
		print(f'Got potato {id(p)}...')

loop = asyncio.get_event_loop()
res = loop.run_until_complete(buy_potatos())
loop.close()

上面的故事讲的是:去超市买土豆,要买20个土豆,每次从货架上拿一个土豆放到篮子里,but,当土豆不够的时候,需要让超市工作人员去加点土豆,不能干等着。

async其实很直白,就是声明异步,是await先动手的。await只能出现在通过async修饰的函数中(这一点跟yield from不一样)。话虽如此,但是一个没有await的异步函数好像也是有点奇怪捏~

6、总结

大概是熟悉了之后就不会脸盲了……

你可能感兴趣的:(06_python)