Python部落(www.freelycode.com)组织翻译, 禁止转载
译者注: async/await 是 python3.5 的新语法,本文中的代码需使用该版本或以上才能正确运行。
在过去几年内,异步编程由于某些好的原因得到了充分的重视。虽然它比线性编程难一点,但是效率相对来说也是更高。
比如,利用 Python 的异步协程 (async coroutine) ,在提交 HTTP 请求后,就没必要等待请求完成再进一步操作,而是可以一边等着请求完成,一边做着其他工作。这可能在逻辑上需要多些思考来保证程序正确运行,但是好处是可以利用更少的资源做更多的事。
即便逻辑上需要多些思考,但实际上在 Python 语言中,异步编程的语法和执行并不难。跟 Javascript 不一样,现在 Python 的异步协程已经执行得相当好了。
对于服务端编程,异步性似乎是 Node.js 流行的一大原因。我们写的很多代码,特别是那些诸如网站之类的高 I/O 应用,都依赖于外部资源。这可以是任何资源,包括从远程数据库调用到 POST 一个 REST 请求。一旦你请求这些资源的任一一个,你的代码在等待资源响应时便无事可做(译者注:如果没有异步编程的话)。
有了异步编程,在等待这些资源响应的过程中,你的代码便可以去处理其他的任务。
协程 (Coroutines)
在 Python 中,异步函数通常被称作协程,创建一个协程仅仅只需使用 async 关键字,或者使用 @asyncio.coroutine 装饰器。下面的任一代码,都可以作为协程工作,形式上也是等同的:
import asyncio
async def ping_server(ip):
pass
@asyncio.coroutine
def load_file(path):
pass
上面这俩特殊的函数,在调用时会返回协程对象。熟悉 JavaScript 中 Promise 的同学,可以把这个返回对象当作跟 Promise 差不多。调用他们中的任意一个,实际上并未立即运行,而是返回一个协程对象,然后将其传递到 Eventloop 中,之后再执行。
如果要判断一个函数是不是协程, asyncio 提供了 asyncio.iscoroutinefunction(func) 方法,如果要判断一个函数返回的是不是协程对象,则可以使用 asyncio.iscoroutine(obj) 。
Yield from
调用协程的方式有有很多, yield from 就是其中的一种。这种方式在 Python3.3 中被引入,在 Python3.5 中以 async/await 的形式进行了优化。yield from 表达式的使用方式如下:
import asyncio
@asyncio.coroutine
def get_jason(client, url):
file_content = yield from load_file('/Usrs/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 必须在调用函数的内部使用(这个调用函数通常被装饰为协程)。
Async/await
较新的语法是使用 async/await 关键字。 async 从 Python3.5 开始被引进,跟 @asyncio.coroutine 装饰器一样,用来声明一个函数是一个协程。只要把它放在函数定义之前,就可以应用到函数上,使用方式如下:
async def ping_server(ip):
# ping code here...
实际调用这个函数时,使用 await 而不用 yield from ,当然,使用方式依然差不多:
async def ping_local(ip):
return await ping_server('192.168.1.1')
再强调一遍,跟 yield from 一样,不能在函数外部使用 await ,否则会抛出语法错误。 (译者注: async 用来声明一个函数是协程,然后使用 await调用这个协程, await 必须在函数内部,这个函数通常也被声明为另一个协程)
Python3.5 对这两种调用协程的方法都提供了支持,但是推荐 async/await 作为首选。
Event Loop
如果你还不知道如何开始和操作一个 Eventloop ,那么上诉有关协程所说的都起不了多大作用。 Eventloop 在执行异步函数时非常重要,重要到只要执行协程,基本上就得利用 Eventloop 。
Eventloop 提供了相当多的功能:注册,执行和取消延迟调用(异步函数)
创建客户端与服务端传输用于通信
创建子程序和通道跟其他的程序进行通信
指定函数的调用到线程池
Eventloop 有相当多的配置和类型可供使用,但大部分程序只需要如下方式预定函数即可:
import asyncio
async def speak_async():
print('OMG asynchronicity!')
loop = asyncio.get_event_loop()
loop.run_until_complete(speak_async())
loop.close()
有意思的是代码中的最后三行,首先获取默认的 Eventloop ( asyncio.get_event_loop() ),然后预定和运行异步任务,并在完成后结束循环。
loop.run_until_complete() 函数实际上是阻塞性的,也就是在所有异步方法完成之前,它是不会返回的。但因为我们只在一个线程中运行这段代码,它没法再进一步扩展,即使循环仍在运行。
可能你现在还没觉得这有多大的用处,因为我们通过调用其他 IO 来结束 Eventloop 中的阻塞(译者注:也就是在阻塞时进行其他 IO ),但是想象一下,如果在网页服务器上,把整个程序都封装在异步函数内,到时就可以同时运行多个异步请求了。
也可以将 Eventloop 的线程中断,利用它去处理所有耗时较长的 IO 请求,而主线程处理程序逻辑或者用户界面。
一个案例
让我们实际操作一个稍大的案例。下面这段代码就是一个非常漂亮的异步程序,它先从 Reddit 抓取 JSON 数据,解析它,然后打印出当天来自 /r/python,/r/programming 和 /r/compsci 的置顶帖。
所示的第一个方法 get_json() ,由 get_reddit_top() 调用,然后只创建一个 GET 请求到适当的网址。当这个方法和 await 一起调用后, Eventloop 便能够继续为其他的协程服务,同时等待 HTTP 响应达到。一旦响应完成, JSON 数据就返回到 get_reddit_top() ,得到解析并打印出来。
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()
这跟我们之前展示出来的代码略有不同。为了达到在 Eventloop 中运行多重协程的目的,我们使用 asyncio.ensure_future() ,然后运行无限循环以处理一切。
为了保证运行成功,你必须先安装 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
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
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! (h
DONE: programming
注意,如果多次运行这段代码,打印出来的 subreddit 数据在顺序上会有些许变化。这是因为每当我们调用一次代码都会释放对线程的控制,容许线程去处理另一个 HTTP 调用。这将导致谁先获得响应,谁就先打印出来。
结论
即使 Python 内置的异步操作没有 Javascript 那么顺畅,但这并不意味着就不能用它来把应用变得更有趣、更有效率。只要花半个小时的时间去了解它的来龙去脉,你就会感觉把异步操作应用到你的程序中将会是多美好的一件事。
英文原文: http://stackabuse.com/python-async-await-tutorial/
译者: Itwonders