协程简介

众所周知,Python在3.5版本引入了async/await语法,在此之前协程由yield/yield from实现。协程的使用方法此处不赘述,说白了就是以await关键字调用被async def 定义的函数。我觉得asyncio应该是所有网络程序的标配,不过初次接触的人可能会被异步程序的跳转搞糊涂,觉得asyncio就是莫名其妙的goto 。对底层实现和控制流程感兴趣的,可以参考这篇文章

阐明概念

本文尝试厘清asyncio里的几个关键概念。首先引进几个包,用async def定义asleep函数,包装一下asyncio.sleep

    import time
    import asyncio
    from typing import Awaitable, Coroutine, Generator
    
    
    async def asleep(duration):
        start = time.perf_counter()
        print(f'Start sleeping at {start} s')
        await asyncio.sleep(duration)
        print(f'Sleep for {time.perf_counter() - start} s')

Coroutine Function

async def定义的函数是Coroutine Function,这一点可以用asyncio.iscoroutinefunction验证。与普通函数不同,给协程函数赋参并不会执行函数里的代码,而是返回一个Coroutine Object。比如

coroutineObj = asleep(1)

Coroutine Object

协程对象(或直接简称协程)可以用asyncio.iscoroutine验证,其类型是typing里的AwaitableCoroutine,结果如下。实现异步并发,就是在Coroutine Object前面以await 关键字调用,如await coroutineObj,这时候协程才真正开始执行。

asyncio.iscoroutinefunction(asleep)=True
asyncio.iscoroutine(coroutineObj)=True
isinstance(coroutineObj, Awaitable)=True
isinstance(coroutineObj, Coroutine)=True

__await__ Attribute

协程对象能被await关键字调用,是因为它们拥有__await__属性,该方法返回的,不是协程函数也不是协程对象,而是一个生成器,验证结果如下。

hasattr(coroutineObj, '__await__')=True
asyncio.iscoroutine(coroutineObj.__await__)=False
asyncio.iscoroutine(coroutineObj.__await__())=False
asyncio.iscoroutinefunction(coroutineObj.__await__)=False
isinstance(coroutineObj.__await__(), Generator)=True

重新实现协程

如果把这三个概念理清楚了,我认为基本上掌握asyncio了。为了让理解更透彻,下面尝试重新实现一个协程,以达到和用async def定义的asleep一样的效果。

    class generatorSleep:
        def __init__(self, duration):
        self.duration = duration

        def __await__(self):
            start = time.perf_counter()
            print(f'Start {type(self).__name__} at {start} s')
            while time.perf_counter() - start < self.duration:
                yield
            print(f'Sleep for {time.perf_counter() - start} s')

首先测试代码如下

    async def main():
        print('Sequential sleep')
        await generatorSleep(1)
        await generatorSleep(2)
        print('Concurrent sleep')
        await asyncio.gather(generatorSleep(1), generatorSleep(2))
        print(f'Finished at {time.perf_counter()} s')
    
    if __name__ == '__main__':
        asyncio.run(main())

测试结果如下,的确实现了异步睡眠。

Sequential sleep
Start generatorSleep at 0.2971936 s
Sleep for 1.0000021000000001 s
Start generatorSleep at 1.297214 s
Sleep for 2.0000028 s
Concurrent sleep
Start generatorSleep at 3.2973044 s
Start generatorSleep at 3.2973155 s
Sleep for 1.0000049999999998 s
Sleep for 2.0000032 s
Finished at 5.297353 s

这里定义的类generatorSleep其实就相当于以async def定义的Coroutine Function,给其赋参后生成了generatorSleep实例,相当于Coroutine Object。 该实例可被await关键字调用,因为其实现了__await__方法,而__await__方法返回的则是一个生成器。

yield from奇技淫巧

需要注意的是,在__await__方法里我选择了以一个while循环加yield的方式来构建生成器,而不是以yield from asyncio.sleep(self.duration)的方式去嵌套生成器。 如果是后者,解析器会弹出

TypeError: cannot 'yield from' a coroutine object in a non-coroutine generator

异常,因为从3.5开始Python刻意在语法上对awaityield from关键字作出了区分,协程不再能被yield from调用,而是必须await。不过实际上, 存在一个workaround,就是先用asyncio.Task包裹协程,再以yield from调用,以下代码可行,asyncio.gather同理。

    class yieldfromSleep:
        def __init__(self, duration):
            self.duration = duration
    
        def __await__(self):
            start = time.perf_counter()
            print(f'Start {type(self).__name__} at {start} s')
            task = asyncio.create_task(asyncio.sleep(self.duration))
            yield from task
            print(f'Sleep for {time.perf_counter() - start} s')

    await yieldfromSleep(1)

至此,一个简陋版的Python协程以类的形式被重新构造,而不需要用async def来定义协程函数。

异步构造函数

理清了协程的构造,接下来介绍如何以异步的方式构造一个类的实例。如果一个类实例的初始化需要从网络获取数据,且需要请求不止一个API,以异步的方式构造实例就能缩短时间,提升效率。

既然协程的异步都来自__await__方法,且该方法可以用yield from的形式来调用其他协程,那事情就简单了。__init__该怎么写还是怎么写, 不过把需要异步并发创建的属性搬到__await__里,在__await__里用yield from调用异步网络API,事情就解决了。例子如下

    class DataClass:
        def __init__(self, **kwargs):
            self.local = kwargs

        def __await__(self):
            task = asyncio.create_task(aiohttp.ClientSession.request(url))
            self.data = yield from task
            return self

    dataInstance = await DataClass(**kwargs)

其实现和上面的重构协程差不多,当DataClass(**kwargs)await调用时,先用__init__方法构建实例,然后await会调用该实例的__await__方法, 剩下的实例属性则在__await__里被异步创建。不过要注意的是,单纯睡眠时__await__不用返回结果,而在这里需要返回self, 否则dataInstance是不会被赋值构建好的实例。

结语

测试代码在此。希望本文能帮读者理清Python asyncio里的几个关键概念。