本文是流畅的python18章的例子,因为我看的是2015版的,不知道最新版有没有修改,关于协程那部分旧的api到现在已经不适用了,我都做了一些修改,保证代码能正常运行,有任何错误欢迎指出来。有关于协程和asynico和aiohttp都在后面都有简单介绍。
协程例子-网络下载
了解协程之前,直接先看2个例子,从例子入手,先感受一下协程,这2个例子也是参考书本,去掉了多线程那个例子,因为本文说的是协程,就直接上协程的例子,关于协程部分如上述做了修改以便能运行。这个例子是网络下载的二种风格,因为网络下载具有很高IO延迟,所以不浪费CPU周期去等待,最好收到响应之前做其他的事。
以下例子环境:
Python v3.7.4
requests v2.23.0
aiohttp 3.7.4.post0
normal.py, 需要在代码运行的当前目录下先创建flags的文件夹。
import os
import time
import sys
import requests
POP20_CC = ('CN IN US ID BR PK NG BD RU JP '
'MX PH VN ET EG DE IR TR CD FR').split()
BASE_URL = 'http://flupy.org/data/flags'
DEST_DIR = './flags'
def save_flag(img, filename):
path = os.path.join(DEST_DIR, filename)
with open(path, 'wb') as fp:
fp.write(img)
def get_flag(cc):
url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())
resp = requests.get(url)
return resp.content
def show(text):
print(text, end=' ')
sys.stdout.flush()
def download_many(cc_list):
for cc in sorted(cc_list):
image = get_flag(cc)
show(cc)
save_flag(image, cc.lower() + '.gif')
return len(cc_list)
def main(download_many):
t0 = time.time()
count = download_many(POP20_CC)
elapsed = time.time() - t0
msg = '\n{} flags downloaded in {:.2f}s'
print(msg.format(count, elapsed))
if __name__ == '__main__':
main(download_many)
运行
python normal.py
BD BR CD CN DE EG ET FR ID IN IR JP MX NG PH PK RU TR US VN
20 flags downloaded in 16.94s
asyncio_test.py, 一样要在代码运行目录下创建flags文件夹,如果nomal.py运行过了,先把文件夹内的图片删干净,再运行asyncio_test.py。
import os
import sys
import time
import asyncio
import aiohttp
POP20_CC = ('CN IN US ID BR PK NG BD RU JP '
'MX PH VN ET EG DE IR TR CD FR').split()
BASE_URL = 'http://flupy.org/data/flags'
DEST_DIR = './flags'
def save_flag(img, filename):
path = os.path.join(DEST_DIR, filename)
with open(path, 'wb') as fp:
fp.write(img)
def show(text):
print(text, end=' ')
sys.stdout.flush()
async def get_flag(cc):
url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
image = await resp.read()
return image
async def download_one(cc):
image = await get_flag(cc)
show(cc)
save_flag(image, cc.lower() + '.gif')
return cc
async def download_many(cc_list):
to_do = [asyncio.create_task(download_one(cc)) for cc in sorted(cc_list)]
done, pending = await asyncio.wait(to_do)
return len(done)
def main():
t0 = time.time()
count = asyncio.run(download_many(POP20_CC))
elapsed = time.time() - t0
msg = '\n{} flags downloaded in {:.2f}s'
print(msg.format(count, elapsed))
if __name__ == '__main__':
main()
python asyncio_test.py
TR EG BR BD ID FR VN JP CN IN RU NG DE MX PH CD ET US IR PK
20 flags downloaded in 0.97s
从这个地方可以看出协程的效果了,顺序下载和协程的下载时间,天差地别。
上述例子是运用了异步IO库asynico, 将介绍它的来由和用法,内容比较多,要了解asynico它的由来,工作流程和运用,得了解生成器原理,了解yied from,最后在到了解asynico一些api。
在流程的python里面,它一步步徐徐展开的,从第十四章就开始从可迭代对象/迭代器到生成器,生成器函数,到十六章开始讲基于生成器的协程,异常处理,yield from,第十八章协程例子正式用到asyncio,秉承这样一步步把这些章节自己所能理解过一遍。
基于生成器的协程
在asynico库里,基于生成器的协程是async/await的前身,并且是用yield from语句的生成器。下面只做简单介绍,因为关于这部分asyncio库里面描述到将来要弃用,但是目前还是兼容的, 它将在python 3.10移除,但是我觉得从它的前身开始理解有好处,清楚它的整个发展。
生成器函数
当一个函数里面有yield语句就是生成器函数,这也是它和普通函数唯一区别,简单看个书本例子:
>>> def gen_123():
... yield 1
... yield 2
... yield 3
>>> gen_123 # doctest: +ELLIPSIS
<function gen_123 at 0x...>
>>> gen_123() # doctest: +ELLIPSIS
<generator object gen_123 at 0x...>
>>> for i in gen_123():
... print(i) 123
>>> g = gen_123()
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
Traceback (most recent call last):
... StopIteration
可以看到直接调用gen_123()是不会有任何执行的,生成器函数会创建一个生成器对象,包装生成器函数的定义体。把生成器传给 next(...) 函数时,生成器函数会向前,执行函数定义体中的 下一个 yield 语句,返回产出的值,并在函数定义体的当前位置暂 停。最终,函数的定义体返回时,外层的生成器对象会抛出 StopIteration 异常——这一点与迭代器协议一致。
生成器进化成协程
协程协程,其实是一个过程,原来的生产器函数是产出值的,每次调用next就产出一个值,但是现在不单单想要生成器函数产出值,还想控制它每次调用到yield语句时给它发送个值从而影响它的逻辑,也就是说这个过程与调用方协作,产出由调用方提供的值。
如果我说的不够清楚,OK,先看下面例子,simple_coro2.
>>> def simple_coro2(a):
... print('-> Started: a =', a)
... b = yield a
... print('-> Received: b =', b)
... c = yield a + b
... print('-> Received: c =', c)
...
>>> my_coro2 = simple_coro2(14)
>>> from inspect import getgeneratorstate
>>> getgeneratorstate(my_coro2)
'GEN_CREATED'
>>> next(my_coro2)
-> Started: a = 14
14
>>> getgeneratorstate(my_coro2)
'GEN_SUSPENDED'
>>> my_coro2.send(28)
-> Received: b = 28
42
>>> my_coro2.send(99)
-> Received: c = 99
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> getgeneratorstate(my_coro2)
'GEN_CLOSED'
从句法上看,协程与生成器类似,都是定义体中包含 yield 关键字的 函数。可是,在协程中,yield 通常出现在表达式的右边(例如,datum = yield),可以产出值,也可以不产出——如果 yield 关键字后面没有表达式,那么生成器产出 None。协程可能会从调用方 接收数据,不过调用方把数据提供给协程使用的是 .send(datum) 方 法,而不是 next(...) 函数。通常,调用方会把值推送给协程。
如果上面例子还是不够直观,直接贴上原来书本的流程图:
三个阶段。
(1) 调用 next(my_coro2),打印第一个消息,然后执行 yield a,产出数字 14。
(2) 调用 my_coro2.send(28),把 28 赋值给 b,打印第二个消息,然后执行 yield a + b,产出数字 42。
(3) 调用 my_coro2.send(99),把 99 赋值给 c,打印第三个消息,协 程终止。
基于 yield from 生成器
要理解yield from记住记住几个关键词,委托,双向通道。
先来看一张图,有包含调用方,委派生成器,自生成器。委派生成器在 yield from 表达式处暂停时,调用方可以直 接把数据发给子生成器,子生成器再把产出的值发给调用方。子生成器返回之后,解释器会抛出 StopIteration 异常,并把返回值附 加到异常对象上,此时委派生成器会恢复
如果能直接理解最好了,有关于yield from语义问题其实也有人提到为什么要那么复杂,我这里就提一下我的理解,不一定正确,提一个现实世界一个例子,有点类似中间商一样,当客户(调用方)有任何需求,可以通过中间商(委派生成器)传达给真正的厂家(自生成器)。
看一下yield from例子,书本上的例子太长,这边借鉴别人的例子,都有注释,参考的连接会放在文章底部:
# 子生成器
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()
还有一点,前面有说道,在asynico库里,基于生成器的协程是async/await的前身,并且是用yield from语句的生成器。基于生成器的协程应该使用 @asyncio.coroutine装饰,虽然并非强制,但使用这样能在一众普通的函数中把协程凸显出来,也有助于调试。
asyncio介绍
推荐高级API使用方式,直接抄两个例子:
例子1:
import asyncio
import time
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
print(f"started at {time.strftime('%X')}")
await say_after(1, 'hello')
await say_after(2, 'world')
print(f"finished at {time.strftime('%X')}")
asyncio.run(main())
started at 17:13:52
hello
world
finished at 17:13:55
例子2:
async def main():
task1 = asyncio.create_task(
say_after(1, 'hello'))
task2 = asyncio.create_task(
say_after(2, 'world'))
print(f"started at {time.strftime('%X')}")
# Wait until both tasks are completed (should take
# around 2 seconds.)
await task1
await task2
print(f"finished at {time.strftime('%X')}")
started at 17:14:32
hello
world
finished at 17:14:34
注意,预期的输出显示代码段的运行时间比之前快了 1 秒,在asyncio里面任务被用来“并行的”调度协程,也就是说当一个协程封装成任务时候,在遇到IO等待时候,会调度其他的任务,达到并行目的。
aiohttp介绍
aiohttp是基于asyncio的,所以要用aiohttp得先按照asyncio。摘抄一段官网的简单例子:
import aiohttp
import asyncio
async def main():
async with aiohttp.ClientSession() as session:
async with session.get('http://httpbin.org/get') as resp:
print(resp.status)
print(await resp.text())
loop = asyncio.get_event_loop()
loop.run_until_complete(main())