为什么Python的coroutine这么特殊

在北邮人论坛上看到一篇关于 Python 中 coroutine 的文章

从一个简单的例子出发,对比了 Lua、Ruby、Python 在实现 coroutine 上的区别和联系:

https://bbs.byr.cn/#!article/Python/22439

这里记录下自己的一些见解

不管是 Lua(stackless):

Lua协程实现

还是 Ruby(stackfull):

Ruby协程实现

它们在首次进入 flatten 后都没有再去调用 resume,而是直接 flatten(elem)

因此 flatten 递归调用自己时并没有创建新的协程栈(应用栈),只在第一个 flatten 协程栈上增加函数帧

因此可以在最顶层的函数帧上直接跳回上一个应用栈(最外层)

同时也可以在最外层的应用栈上直接跳到下一协程栈的最顶层函数帧。

也就是说其始终只有两个应用栈,只不过 Lua 的两个应用栈对应的两个C帧在同一个C栈

而 Ruby 的两个协程栈对应了两个不同的C栈(可以在C栈间转跳):

flatten 递归

而 Python(stackfull) 很特殊:

Python 创建协程

它既不是 stackless(在同一C栈上创建新的C帧来创建新的协程栈)

也没实现在C栈间转跳(创建新的C栈来创建新的协程栈)

因此其只能在同一个C栈上创建新的C帧来创建新的协程帧

这些协程帧复用了指向上一帧的指针来表示指向自己的父协程帧,因此 Python 中创建协程就相当于创建函数

但其 yield 时只消除对应的C帧而保留协程帧

因此 python 中的协程无法一次性跳回最外层的应用帧(首次发送send的帧)

只能一层一层地往外(下) yield,并且也只能一层一层地往内(上)send

体现在代码上就是要多一层 for 循环来帮助内外连通

想想 for 的实现机制,for 本身在不断 next 和 捕获最后的 StopIteration,至于 yield 需要自己在 for 的代码块中写

利用 yield from 可以简写为:

def flatten(obj):
    if isinstance(obj, list):
        for elem in obj:
            yield from flatten(elem)
    else:
        yield obj

引入 yield from 的目的就是为了屏蔽那个多出来一层的让人难以理解的for循环,同时又能实现内外连通

yield from 的实现机制可参考《流畅的Python》第16章

更加详细的描述见 PEP 380 – Syntax for Delegating to a Subgenerator

最后,可以使用 http://www.pythontutor.com/visualize.html#mode=edit 来可视化整个执行流

————

更新:学长又出了一篇很棒的番外~

https://bbs.byr.cn/#!article/Python/22645

文章目录
|