Python async/await

发布时间:2019-10-12 20:09:16编辑:auto阅读(1847)

    原文链接:
    http://stackabuse.com/python-async-await-tutorial/?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io

    过去几年,异步编程方式被越来越多的程序员使用, 当然这是有原因的。 尽管异步编程比顺序编程更难, 但是它也更高效。

    在顺序编程中, 发起一个HTTP请求需要阻塞以等待他的返回结果, 使用异步编程你可以发起这个HTTP请求, 然后在等待结果返回的同时做一些其他的事情,等待结果的协程会被放在一个队列里面。 为了保证逻辑的正确性, 这可能会需要考虑的更多, 但是这也使我们用更少的资源处理更多的事情。
    Python中的异步语法和调用并不难。 和Javascript的异步编程比起来可能有点难, 不过也还好。

    异步的处理方式可能解释了为什么Node.js在服务器端这么流行。 我们的代码依赖于外部的一些资源, 尤其是IO密集型的应用, 比如网站可能需要从数据库调用中向一个REST接口POST数据。 一旦我们请求一些外部资源, 我们的代码就要阻塞, 而不能处理别的逻辑。
    利用异步编程, 我们可以在等待其他资源返回的时候, 做一些其他的事情。

    Coroutines

    在python中,异步函数被称作协程: 使用async关键字 或者利用@asyncio.coroutine装饰器。 下面的两种形式是等效的:

    import asyncio
    
    async def ping_server(ip):  
        pass
    
    @asyncio.coroutine
    def load_file(path):  
        pass

    上面的函数调用的时候返回的是一个协程的对象。 如果你熟悉javascript, 你可以认为这个返回对象就像javascript里面的Promise。 现在调用这两个函数, 是不能执行的, 仅仅返回的是一个协程对象, 这个对象可以被用来在后面的event loop中使用。

    如果你想知道一个函数是不是协程, asyncio提供的asyncio.iscoroutine(obj)函数可以帮助你。

    Yield from

    有几种调用协程的方式,其中一种是使用yield from方法。 yield from在Python3.3中被引进, 在Python3.5的async/await(我们后面会提到) 得到进一步的扩展。
    yield from表达式可以用如下的方式使用:

    import asyncio
    
    @asyncio.coroutine
    def get_json(client, url):  
        file_content = yield from load_file('/Users/scott/data.txt')

    如上, yield from在有@asyncio.coroutine装饰器的函数中使用的示例。 如果你在函数外面使用yield from, 你会得到下面的错误:

    File "main.py", line 1
        file_content = yield from load_file('/Users/scott/data.txt')
                      ^
    SyntaxError: 'yield' outside function  

    必须在函数中使用yield from, 典型的用法是在有@asyncio.coroutine装饰器的函数种使用。

    Async/await

    更新、更方便的语法是使用async/await关键字。async关键字是在Python3.5引入的, 被用来修饰一个函数, 让其成为协程, 和@asyncio.coroutine功能类似。 使用如下:

    async def ping_server(ip):  
        # ping code here...

    调用这个函数, 使用await, 而不是yield from, 不过方式差不多:

    async def ping_local():  
        return await ping_server('192.168.1.1')

    你不能在一个协程外面使用await关键字, 否则会得到语法错误。 就像yield from不能在函数外面使用一样。

    Python3.5中, 上面两种协程声明的方式都支持, 但是首选async/await方式。

    Running the event loop

    上面描述的协程例子都不会正常的运行, 如果要运行, 需要用到event loop.。event loop是协程执行的控制点, 如果你希望执行协程, 就需要用到它们。

    event loop提供了如下的特性:

    • 注册、执行、取消延时调用(异步函数)

    • 创建用于通信的client和server协议(工具)

    • 创建和别的程序通信的子进程和协议(工具)

    • 把函数调用送入线程池中

    有一些配置和event loop的类型你可以使用, 但是如果你想去执行一个函数, 可以使用下面的配置, 而且在大多数场景中这样就够了:

    import asyncio
    
    async def speak_async():  
        print('OMG asynchronicity!')
    
    loop = asyncio.get_event_loop()  
    loop.run_until_complete(speak_async())  
    loop.close()  

    最后三行是重点。 asyncio启动默认的event loop(asyncio.get_event_loop()), 调度并执行异步任务, 关闭event loop。

    loop.run_until_complete()这个函数是阻塞执行的, 直到所有的异步函数执行完毕。 因为我们的程序是单线程运行的, 所以, 它没办法调度到别的线程执行。

    你可能会认为这不是很有用, 因为我们的程序阻塞在event loop上(就像IO调用), 但是想象一下这样: 我们可以把我们的逻辑封装在异步函数中, 这样你就能同时执行很多的异步请求了, 比如在一个web服务器中。

    你可以把event loop放在一个单独的线程中, 让它执行IO密集型的请求, 而主线程可以继续处理程序逻辑或者UI渲染。

    An example

    OK, 让我看一个稍微长一点的例子, 这个例子是可以实际运行的。 例子是一个简单的从Reddit的/r/python, /r/programming, and /r/compsci页面异步获取JSON数据, 解析, 打印出这些页面发表的文章。

    get_json()方法是被get_reddit_top()调用的, get_reddit_top()发起了一个HTTP GET请求到Reddit。 当调用被await修饰, event loop就会继续在等待请求返回的时候处理其他的协程。 一旦请求返回, JSON数据会被返回get_reddit_top(), 然后解析, 打印。

    import signal  
    import sys  
    import asyncio  
    import aiohttp  
    import json
    
    loop = asyncio.get_event_loop()  
    client = aiohttp.ClientSession(loop=loop)
    
    async def get_json(client, url):  
        async with client.get(url) as response:
            assert response.status == 200
            return await response.read()
    
    async def get_reddit_top(subreddit, client):  
        data1 = await get_json(client, 'https://www.reddit.com/r/' + subreddit + '/top.json?sort=top&t=day&limit=5')
    
        j = json.loads(data1.decode('utf-8'))
        for i in j['data']['children']:
            score = i['data']['score']
            title = i['data']['title']
            link = i['data']['url']
            print(str(score) + ': ' + title + ' (' + link + ')')
    
        print('DONE:', subreddit + '\n')
    
    def signal_handler(signal, frame):  
        loop.stop()
        client.close()
        sys.exit(0)
    
    signal.signal(signal.SIGINT, signal_handler)
    
    asyncio.ensure_future(get_reddit_top('python', client))  
    asyncio.ensure_future(get_reddit_top('programming', client))  
    asyncio.ensure_future(get_reddit_top('compsci', client))  
    loop.run_forever()  

    这个程序和上面展示的例子有一点不同。 我们使用asyncio.ensure_future()让event loop处理多个协程, 然后让event loop一直执行, 直到处理了所有的请求。

    为了执行这个程序, 需要安装aiohttp, 你可以用pip来安装:

    pip install aiohttp

    要保证这个程序运行在python3.5以后的版本, 输出的结果如下:

    $ python main.py
    46: Python async/await Tutorial (http://stackabuse.com/python-async-await-tutorial/)  
    16: Using game theory (and Python) to explain the dilemma of exchanging gifts. Turns out: giving a gift probably feels better than receiving one... (http://vknight.org/unpeudemath/code/2015/12/15/The-Prisoners-Dilemma-of-Christmas-Gifts/)  
    56: Which version of Python do you use? (This is a poll to compare the popularity of Python 2 vs. Python 3) (http://strawpoll.me/6299023)  
    DONE: python
    
    71: The Semantics of Version Control - Wouter Swierstra (http://www.staff.science.uu.nl/~swier004/Talks/vc-semantics-15.pdf)  
    25: Favorite non-textbook CS books (https://www.reddit.com/r/compsci/comments/3xag9e/favorite_nontextbook_cs_books/)  
    13: CompSci Weekend SuperThread (December 18, 2015) (https://www.reddit.com/r/compsci/comments/3xacch/compsci_weekend_superthread_december_18_2015/)  
    DONE: compsci
    
    1752: 684.8 TB of data is up for grabs due to publicly exposed MongoDB databases (https://blog.shodan.io/its-still-the-data-stupid/)  
    773: Instagram's Million Dollar Bug? (http://exfiltrated.com/research-Instagram-RCE.php)  
    387: Amazingly simple explanation of Diffie-Hellman. His channel has tons of amazing videos and only a few views :( thought I would share! (https://www.youtube.com/watch?v=Afyqwc96M1Y)  
    DONE: programming  

    如果你多运行几次这个程序, 得到的输出结果是不一样的。 这是因为我们调用的协程的同时, 允许其他的HTTP请求执行。 结果最先返回的请求最先打印出来。

    总结

    尽管Python内置的异步函数使用起来没有Javascript中的那么简便, 不过, 这不意味着它不能使应用更有趣和高效。 花费30分钟去学习异步相关的知识, 你就能更好的把它应用在你的项目中。

关键字