作为 Python 核心开发者之一,让我很想了解这门语言是如何运作的。我发现总有一些阴暗的角落我对其中错综复杂的细节不是很清楚,但是为了能够有助于 Python 的一些问题和其整体设计,我觉得我应该试着去理解 Python 的核心语法和内部运作机制。
但是直到最近我才理解 Python 3.5 中 async
/await
的原理。我知道 Python 3.3 中的 yield from
和 Python 3.4 中的 asyncio
组合得来这一新语法。但较少处理网络相关的问题 – asyncio
并不仅限于此但确是重要用途 – 使我没太注意 async
/await
。我知道:
1 |
yield from iterator |
(本质上)相当于:
1 2 |
for x in iterator: yield x |
我知道 asyncio
是事件循环框架可以进行异步编程,但是我只是知道这里面每个单词的意思而已,从没深入研究 async
/await
语法组合背后的原理,我发现不理解 Python 中的异步编程已经对我造成了困扰。因此我决定花时间弄清楚这背后的原理究竟是什么。我从很多人那里得知他们也不了解异步编程的原理,因此我决定写这篇论文(是的,由于这篇文章花费时间之久以及篇幅之长,我的妻子已经将其定义为一篇论文)。
由于我想要正确地理解这些语法的原理,这篇文章涉及到一些关于 CPython 较为底层的技术细节。如果这些细节超出了你想了解的内容,或者你不能完全理解它们,都没关系,因为我为了避免这篇文章演变成一本书那么长,省略了一些 CPython 内部的细枝末节(比如说,如果你不知道 code object 有 flags,甚至不知道什么是 code object,这都没关系,也不用一定要从这篇文字中获得什么)。我试着在最后一小节中用更直接的方法做了总结,如果觉得文章对你来说细节太多,你完全可以跳过。
关于 Python 协程的历史课
根据维基百科给出的定义,“协程 是为非抢占式多任务产生子程序的计算机程序组件,协程允许不同入口点在不同位置暂停或开始执行程序”。从技术的角度来说,“协程就是你可以暂停执行的函数”。如果你把它理解成“就像生成器一样”,那么你就想对了。
退回到 Python 2.2,生成器第一次在PEP 255中提出(那时也把它成为迭代器,因为它实现了迭代器协议)。主要是受到Icon编程语言的启发,生成器允许创建一个在计算下一个值时不会浪费内存空间的迭代器。例如你想要自己实现一个 range()
函数,你可以用立即计算的方式创建一个整数列表:
1 2 3 4 5 6 7 8 |
def eager_range(up_to): """Create a list of integers, from 0 to up_to, exclusive.""" sequence = [] index = 0 while index < up_to: sequence.append(index) index += 1 return sequence |
然而这里存在的问题是,如果你想创建从0到1,000,000这样一个很大的序列,你不得不创建能容纳1,000,000个整数的列表。但是当加入了生成器之后,你可以不用创建完整的序列,你只需要能够每次保存一个整数的内存即可。
1 2 3 4 5 6 |
def lazy_range(up_to): """Generator to return the sequence of integers from 0 to up_to, exclusive.""" index = 0 while index < up_to: yield index index += 1 |
让函数遇到 yield
表达式时暂停执行 – 虽然在 Python 2.5 以前它只是一条语句 – 并且能够在后面重新执行,这对于减少内存使用、生成无限序列非常有用。
你有可能已经发现,生成器完全就是关于迭代器的。有一种更好的方式生成迭代器当然很好(尤其是当你可以给一个生成器对象添加 __iter__()
方法时),但是人们知道,如果可以利用生成器“暂停”的部分,添加“将东西发送回生成器”的功能,那么 Python 突然就有了协程的概念(当然这里的协程仅限于 Python 中的概念;Python 中真实的协程在后面才会讨论)。将东西发送回暂停了的生成器这一特性通过 PEP 342添加到了 Python 2.5。与其它特性一起,PEP 342 为生成器引入了 send()
方法。这让我们不仅可以暂停生成器,而且能够传递值到生成器暂停的地方。还是以我们的 range()
为例,你可以让序列向前或向后跳过几个值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
def jumping_range(up_to): """Generator for the sequence of integers from 0 to up_to, exclusive. Sending a value into the generator will shift the sequence by that amount. """ index = 0 while index < up_to: jump = yield index if jump is None: jump = 1 index += jump if __name__ == '__main__': iterator = jumping_range(5) print(next(iterator)) # 0 print(iterator.send(2)) # 2 print(next(iterator)) # 3 print(iterator.send(-1)) # 2 for x in iterator: print(x) # 3, 4 |
直到PEP 380 为 Python 3.3 添加了 yield from
之前,生成器都没有变动。严格来说,这一特性让你能够从迭代器(生成器刚好也是迭代器)中返回任何值,从而可以干净利索的方式重构生成器。
1 2 3 4 5 6 7 8 |
但是为了能够有助于 Python 的一些问题和其整体设计,我觉得我应该试着去理解 Python 的核心语法和内部运作机制。
但是直到最近我才理解 Python 3.5 中
(本质上)相当于:
我知道 由于我想要正确地理解这些语法的原理,这篇文章涉及到一些关于 CPython 较为底层的技术细节。如果这些细节超出了你想了解的内容,或者你不能完全理解它们,都没关系,因为我为了避免这篇文章演变成一本书那么长,省略了一些 CPython 内部的细枝末节(比如说,如果你不知道 code object 有 flags,甚至不知道什么是 code object,这都没关系,也不用一定要从这篇文字中获得什么)。我试着在最后一小节中用更直接的方法做了总结,如果觉得文章对你来说细节太多,你完全可以跳过。 关于 Python 协程的历史课根据维基百科给出的定义,“协程 是为非抢占式多任务产生子程序的计算机程序组件,协程允许不同入口点在不同位置暂停或开始执行程序”。从技术的角度来说,“协程就是你可以暂停执行的函数”。如果你把它理解成“就像生成器一样”,那么你就想对了。 退回到 Python 2.2,生成器第一次在PEP 255中提出(那时也把它成为迭代器,因为它实现了迭代器协议)。主要是受到Icon编程语言的启发,生成器允许创建一个在计算下一个值时不会浪费内存空间的迭代器。例如你想要自己实现一个
然而这里存在的问题是,如果你想创建从0到1,000,000这样一个很大的序列,你不得不创建能容纳1,000,000个整数的列表。但是当加入了生成器之后,你可以不用创建完整的序列,你只需要能够每次保存一个整数的内存即可。
让函数遇到 你有可能已经发现,生成器完全就是关于迭代器的。有一种更好的方式生成迭代器当然很好(尤其是当你可以给一个生成器对象添加
直到PEP 380 为 Python 3.3 添加了
|