开源中国

我们不支持 IE 10 及以下版本浏览器

It appears you’re using an unsupported browser

为了获得更好的浏览体验,我们强烈建议您使用较新版本的 Chrome、 Firefox、 Safari 等,或者升级到最新版本的IE浏览器。 如果您使用的是 IE 11 或以上版本,请关闭“兼容性视图”。
玩转 Python 3.5 的 await/async - 技术翻译 - 开源中国社区

玩转 Python 3.5 的 await/async 【已翻译100%】

kajhsdjkah 推荐于 3年前 (共 5 段, 翻译完成于 11-05) 评论 19
收藏  
95
推荐标签: Python 待读
参与翻译 (1人) : zicode 仅中文 | 中英文对照 | 仅英文 | 打印此文章

最近通过的PEP-0492为 Python 3.5 在处理协程时增加了一些特殊的语法。新功能中很大一部分在3.5 之前的版本就已经有了,不过之前的语法并不算最好的,因为生成器和协程的概念本身就有点容易混淆。PEP-0492 通过使用 async 关键字显式的对生成器和协程做了区分。

本文旨在说明这些新的机制在底层是如何工作的。如果你只是对怎么使用这些功能感兴趣,那我建议你可以忽略这篇文章,而是去看一下内置的 asyncio 模块的文档。如果你对底层的概念感兴趣,关心这些底层功能如何能构建你自己的 asyncio 模块,那你会发现本文会有有意思。

本文中我们会完全放弃任何异步 I/O 方法,而只限于使用多协程的交互。下面是两个很小的函数:

def coro1():
    print("C1: Start")
    print("C1: Stop")


def coro2():
    print("C2: Start")
    print("C2: a")
    print("C2: b")
    print("C2: c")
    print("C2: Stop")

我们从两个最简单的函数开始,coro1和coro2。我们可以按顺序来执行这两个函数:

coro1()
coro2()

我们得到期望的输出结果:

C1: Start
C1: Stop
C2: Start
C2: a
C2: b
C2: c
C2: Stop

不过,基于某些原因,我们可能会期望这些代码交互运行。普通的函数做不到这点,所以我们把这些函数转换成携程:

async def coro1():
    print("C1: Start")
    print("C1: Stop")


async def coro2():
    print("C2: Start")
    print("C2: a")
    print("C2: b")
    print("C2: c")
    print("C2: Stop")
zicode
 翻译得不错哦!

通过新的 async 关键字的魔法,这些函数不再是函数了,现在它们变成了协程(更准确的说是本地协程函数)。普通函数被调用的时候,函数体会被执行,但是在调用协程函数的时候,函数体并不会被执行,你得到的是一个协程对象:

c1 = coro1()
c2 = coro2()
print(c1, c2)

输出:

<coroutine object coro1 at 0x10ea60990> <coroutine object coro2 at 0x10ea60a40>

 (解释器还会打印一些运行时的警告信息,先忽略掉)。

那么,为什么要有一个协程对象?代码到底如何执行?执行协程的一种方式是使用 await 表达式(使用新的 await 关键字)。你可能会想,可以这样来做:

await c1

不过,你肯定会失望了。await 表达式只有在本地协程函数里才是有效的。你必须这样做:

async def main():
    await c1

接下来问题来了,main 函数又是如何开始执行的呢?

关键之处是协程确实是与 Python 的生成器非常相似,也都有一个 send 方法。我们可以通过调用 send 方法来启动一个协程的执行。

c1.send(None)

这样我们的第一个协程终于可以执行完成了,不过我们也得到了一个讨厌的 StopIteration 异常:

C1: Start
C1: Stop
Traceback (most recent call last):
  File "test3.py", line 16, in 
    c1.send(None)
StopIteration

StopIteration 异常是一种标记生成器(或者像这里的协程)执行结束的机制。虽然这是一个异常,但是确实是我们期望的!我们可以用适当的 try-catch 代码将其包起来,这样就可以避免错误提示。接下来我们让我们的第二个协程也执行起来:

try:
    c1.send(None)
except StopIteration:
    pass
try:
    c2.send(None)
except StopIteration:
    pass
zicode
 翻译得不错哦!

现在我们得到了全部的输出,不过有点让人失望的是这跟最初的输出结果没有啥区别。因此我们增加了不少代码,不过还没有做到交替执行。协程与线程相似的地方是多个线程之间也可以交替执行,不过与线程不同之处在于协程之间的切换是显式的,而线程是隐式的(大多数情况下是更好的方式)。所以我们需要加入显式切换的代码。

通常生成器的 send 方法会一直运行,直到通过 yield 关键字放弃执行,也许你认为我们的 coro1 可以改成这个样子:

async def coro1():
    print("C1: Start")
    yield
    print("C1: Stop")

但是我们不能在协程里使用 yield。作为替换,我们可以使用新的 await 表达式来暂停协程的执行,直到 awaitable 执行结束。于是我们需要的代码类似于 await _something_;问题是这里 _something_ 是什么呢?我们必须 await 某个东西,而不是空!这个 PEP 解释了什么是可以 await 的(awaitable)。其中一种是另一个本地协程,不过这个对我们了解底层细节没有啥帮助。另一种是通过特定 CPython API 定义的对象,不过我们暂时还不打算引入扩展模块,而只限于使用纯 Python。除此之外,还剩下两种选择:基于生成器的协程对象,或者一个特殊的类似 Future 的对象。

zicode
 翻译得不错哦!

接下来,我们会选择基于生成器的协程对象。基本上一个 Python 的生成器(例如:某个有yield表达式的函数)可以通过 types.coroutine 装饰被标记成一个协程。所以,这是一个最简单的例子:

@types.coroutine
def switch():
    yield

这定义了一个基于生成器的协程函数。要得到基于生成器的协程对象,只需要执行这个函数。我们可以把我们的 coro1 协程修改成下面这样:

async def coro1():
    print("C1: Start")
    await switch()
    print("C1: Stop")

通过上面的修改,我们期望 coro1 和 coro2 可以交错执行。到目前为止,输出是这样的:

C1: Start
C2: Start
C2: a
C2: b
C2: c
C2: Stop

我没看到正如期望的,在第一条打印语句之后,coro1 停止执行,coro2 接着执行。实际上,我们可以通过下面的代码查看协程对象是如何暂停执行的:

print("c1 suspended at: {}:{}".format(c1.gi_frame.f_code.co_filename, c1.gi_frame.f_lineno))

这可以打印 await 表达式所在的行。(注意:打印的是最外层的 await,所以这里只是起示例作用,通常情况下用处不大)。

zicode
 翻译得不错哦!

现在的问题是,如何让 coro1 继续执行完呢?我们可以再调用一次 send,代码如下:

try:
    c1.send(None)
except StopIteration:
    pass
try:
    c2.send(None)
except StopIteration:
    pass
try:
    c1.send(None)
except StopIteration:
    pass

得到的输出跟预期一样:

C1: Start
C2: Start
C2: a
C2: b
C2: c
C2: Stop
C1: Stop

目前,我们通过为不同的协程显式调用 send来让它们都执行结束。通常情况下这种方式不是很好。我们希望的是有一个函数来控制所有的协程的运行,直到全部协程都执行完成。换句话说,我们期望连续不断的调用 send,驱动不同的协程去执行,直到send抛出 StopIteration 异常。

为此我们新建一个函数,这个函数传入一个协程列表,函数执行这些协程直到全部结束。我们现在要做的就是调用这个函数。

def run(coros):
    coros = list(coros)

    while coros:
        # Duplicate list for iteration so we can remove from original list.
        for coro in list(coros):
            try:
                coro.send(None)
            except StopIteration:
                coros.remove(coro)

这段代码每次从协程列表里取一个协程执行,如果捕获到 StopIteration 异常,就把这个协程从队列里去掉。

接下来我们把手工调用 send 的代码去掉,代码如下:

c1 = coro1()
c2 = coro2()
run([c1, c2])

综上所述,在 Python 3.5,我们现在可以通过新的 await 和 async 功能很轻松的执行协程。本文的相关代码可以在 github 上找到。

zicode
 翻译得不错哦!
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们
评论(19)
Ctrl/CMD+Enter

js也有13
怎么就觉着那么别扭呢~~

引用来自“码德”的评论

怎么就觉着那么别扭呢~~
异步编程都这样,至少比JS的无尽回调好多了
同样是回调有人写的清晰易懂,有人写出了 Callback Hell,主要还是人的因素,不过 ES7 中也准备加入 await 和 async 的支持。
js抄的python,比如迭代器生成器的概念就是直接抄过来的。
继续努力,争取语法赶上.net,这样还是有点负责
怎么实现go的那种
真费劲,不如go
python的特性也越来越多了……
这样的实现真的很丑
这个特性是抄c#的?

引用来自“kongkongyzt”的评论

这样的实现真的很丑
还是 Promise/A+ 的实现平易近人~
所以我们把这些函数转换成携程: 广告13

引用来自“码德”的评论

怎么就觉着那么别扭呢~~

引用来自“Mallon”的评论

异步编程都这样,至少比JS的无尽回调好多了

引用来自“王艳青”的评论

哈哈哈
啦啦啦啦

引用来自“疯狂的小企鹅”的评论

所以我们把这些函数转换成携程: 广告13
fsfdsfdsfd
别在迭代容器 list/dict 时候增减容器内的元素:
Python 3.5.0
>>> aa = [1, 2, 3, 4, 5, 6]
>>> for a in aa:
>>> if 2 < a < 5:
>>> aa.remove(a)
>>> aa
[1, 2, 4, 5, 6]
距离golang的携程还有点远
真的感觉别扭!
整体翻译得不错,但有些地方直译过来反而有点让人困惑
顶部