Python 3.5 协程究竟是个啥

404 查看

作为 Python 核心开发者之一,让我很想了解这门语言是如何运作的。我发现总有一些阴暗的角落我对其中错综复杂的细节不是很清楚,但是为了能够有助于 Python 的一些问题和其整体设计,我觉得我应该试着去理解 Python 的核心语法和内部运作机制。

但是直到最近我才理解 Python 3.5 中 async/await 的原理。我知道 Python 3.3 中的 yield fromPython 3.4 中的 asyncio 组合得来这一新语法。但较少处理网络相关的问题 – asyncio 并不仅限于此但确是重要用途 – 使我没太注意 async/await 。我知道:

(本质上)相当于:

我知道 asyncio 是事件循环框架可以进行异步编程,但是我只是知道这里面每个单词的意思而已,从没深入研究 async/await 语法组合背后的原理,我发现不理解 Python 中的异步编程已经对我造成了困扰。因此我决定花时间弄清楚这背后的原理究竟是什么。我从很多人那里得知他们也不了解异步编程的原理,因此我决定写这篇论文(是的,由于这篇文章花费时间之久以及篇幅之长,我的妻子已经将其定义为一篇论文)。

由于我想要正确地理解这些语法的原理,这篇文章涉及到一些关于 CPython 较为底层的技术细节。如果这些细节超出了你想了解的内容,或者你不能完全理解它们,都没关系,因为我为了避免这篇文章演变成一本书那么长,省略了一些 CPython 内部的细枝末节(比如说,如果你不知道 code object 有 flags,甚至不知道什么是 code object,这都没关系,也不用一定要从这篇文字中获得什么)。我试着在最后一小节中用更直接的方法做了总结,如果觉得文章对你来说细节太多,你完全可以跳过。

关于 Python 协程的历史课

根据维基百科给出的定义,“协程 是为非抢占式多任务产生子程序的计算机程序组件,协程允许不同入口点在不同位置暂停或开始执行程序”。从技术的角度来说,“协程就是你可以暂停执行的函数”。如果你把它理解成“就像生成器一样”,那么你就想对了。

退回到 Python 2.2,生成器第一次在PEP 255中提出(那时也把它成为迭代器,因为它实现了迭代器协议)。主要是受到Icon编程语言的启发,生成器允许创建一个在计算下一个值时不会浪费内存空间的迭代器。例如你想要自己实现一个 range() 函数,你可以用立即计算的方式创建一个整数列表:

然而这里存在的问题是,如果你想创建从0到1,000,000这样一个很大的序列,你不得不创建能容纳1,000,000个整数的列表。但是当加入了生成器之后,你可以不用创建完整的序列,你只需要能够每次保存一个整数的内存即可。

让函数遇到 yield 表达式时暂停执行 – 虽然在 Python 2.5 以前它只是一条语句 – 并且能够在后面重新执行,这对于减少内存使用、生成无限序列非常有用。

你有可能已经发现,生成器完全就是关于迭代器的。有一种更好的方式生成迭代器当然很好(尤其是当你可以给一个生成器对象添加 __iter__() 方法时),但是人们知道,如果可以利用生成器“暂停”的部分,添加“将东西发送回生成器”的功能,那么 Python 突然就有了协程的概念(当然这里的协程仅限于 Python 中的概念;Python 中真实的协程在后面才会讨论)。将东西发送回暂停了的生成器这一特性通过 PEP 342添加到了 Python 2.5。与其它特性一起,PEP 342 为生成器引入了 send() 方法。这让我们不仅可以暂停生成器,而且能够传递值到生成器暂停的地方。还是以我们的 range() 为例,你可以让序列向前或向后跳过几个值:

直到PEP 380Python 3.3 添加了 yield from之前,生成器都没有变动。严格来说,这一特性让你能够从迭代器(生成器刚好也是迭代器)中返回任何值,从而可以干净利索的方式重构生成器。