最初的思考:那么发电机是迭代器,但是旋ou呢?
在last post中,我们介绍了发电机作为迭代器的表现。但是在帖子的最后一部分中,我们说我们将讨论如何将发电机的接口连接到Python中简单的Coroutine对象的概念。
正如我们已经看到的那样,PEP 342试图将发电机概念扩展到Coroutine。如果您阅读了详细信息,则可以看到yield
关键字现在被视为表达式(也就是说,它本身可以被视为R值)。
所以而不是
yield foo
我们现在可以将其视为
bar = (yield foo)
也有一种重要的方法来介绍,即send()
。这两个-yield
表达式和send()
是将发电机功能扩展到Python中的Coroutines的两个主要元素。
但是,在我们直接处理send()
之前,让我们回顾一下__next__()
在发电机内的工作方式。
评论:__next__()
的工作原理
请参阅下面的此简单示例代码:
def example():
print("### start ###")
a = yield 1
print(2)
print("a:", a)
b = yield 3
print(4)
print("b: ", b)
print("### end ###")
if __name__ == "__main__":
gen = example()
print(“first call start”)
print("first call: ", next(gen))
print(“second call start”)
print("second call: ", next(gen))
print(“third call start”)
print("third call: ", next(gen))
如果您运行上面的代码,我们将有这样的结果:
first call start
### start ###
first call: 1
second call start
2
a: None
second call: 3
third call start
4
b: None
### end ###
Traceback (most recent call last):
File “<my_python_file_path>”, line 17, in <module>
print("third call: ", next(gen))
StopIteration
因此,您可以看到__next__()
在发电机函数example
产生控制流程后立即停止。为了方便起见,我将这种情况可视化如下:
-
停止搬到下一行,等待
__next__()
调用,以便比2
更早打印了2
和2
2 -
也在这里停止下一个
__next__()
,因此在这里third call start
比4
和b: None
更早打印。
在下一个部分中,我们将看到send()
方法和yield
表达式与__next__()
有关。
yield
表达和send()
您可能已经注意到,在上述代码中,a
和b
的变量为None
。这是故意的,因为我们想介绍yield
表达式如何以__next__()
的表现。但是,这看起来很奇怪,因为a
和b
均分配了None
。
实际上,根据PEP 342的说法,__next__()
与send(None)
完全相同(您也可以从this CPython code line确认这一点)。因此,我们代码中的next(gen)
实际上是gen.send(None)
,而该None
成为yield
表达式yield 1
和yield 2
的值,然后分别分配给a
和b
。这就是Python团队将发电机扩展到Coroutines的方式:现在发电机只是Coroutines的特殊情况。
但是,这种send()
方法的行为如何使next() = send(None)
有意义?
根据documentation of koude1,它恢复了执行,并将其论点传递给了产量表达。
因此,如果您看到以下代码行:
def example_coroutine():
yielded_value = "yielded"
sended_value = yield yielded_value
print(sended_value)
if __name__ == “__main__”:
coro = example_coroutine()
print(coro.send(None)) # same as next(coro)
print(“before send…”)
coro.send(“sended”)
然后结果将是:
yielded
before send…
sended
# omitted: Traceback and StopIteration Exception
在这里,send(“sended”)
实际上是在sended_value = yield yielded_value
停止的控制流程后的。这就是为什么我们比”sended”
更早打印的”before send…”
消息。现在,弦”sended”
分配给sended_value
并在example_coroutine
coroutine主体内部打印出来。
但是,您还会注意到我们首先致电coro.send(None)
。除了一个None
值,python解释器都引发了例外:
can't send non-None value to a just-started generator
这是由Python本身设计的。如您所见,为了调用send()
,我们需要恢复代码并将send()
的论点传递给屈服表达式。但是,由于coroutine函数(=生成器函数)在没有这样停止的yield
表达式的情况下开始,如果我们首次称为coroutine的send()
,我们可能会浪费send()
的参数值。因此,Python指示首先使用send(None)
启动Coroutine(请参阅this CPython code-如果您致电send(<not_none_value>)
,则将获得与源代码中写的完全相同的错误消息。
我们可以如下可视化上面的过程(现在将箭头放在=
运算符下方是没有意义的吗?)。请注意,该代码有点增加,以进行更详细的说明:
- 我们首先发送
None
来引导我们的coroutine -
yielded_value
进入主要例程(即称为send
的功能),而Coroutine在这里停止 - 值
”yielded”
被打印 - 现在我们发送任何非
None
值 - 这里的字符串”sended”
发送到coroutine - 现在,控制流从前暂停的位置恢复了(2)。
”sended”
的值分配给sended_value
,其余的行才会在我们遇到另一个yield
表达式之前执行-yield yielded_value2
- 然后
yielded_value2
进入主要例程 - 字符串
”yielded_value2”
被打印,其余过程一直持续到Coroutine获得StopIterations
例外,这意味着它没有更多的值
哇,有点令人困惑,但这就是!
为了您的兴趣,如果我们从上一节中调整代码,用send()
替换next()
:
def example():
print("### start ###")
a = yield 1
print(2)
print("a:", a)
b = yield 3
print(4)
print("b: ", b)
print("### end ###")
if __name__ == "__main__":
co = example()
print(“coroutine init: ”, co.send(None))
print(“first call start”)
print("first call: ", co.send(2.5))
print(“second call start”)
print("second call: ", co.send(4.5))
然后结果将是:
### start ###
coroutine init: 1
first call start
2
a: 2.5
first call: 3
second call start
4
b: 4.5
### end ###
// Traceback messages omitted
通往异步的道路
现在,“coroutine” in the Python glossary一词很有意义。我们可以将数据发送给它,并在几个点(= send
方法调用)中获得一些值。
,但我们还没有完成。请注意,我将这个发电机称为Coroutine,为简单的Coroutine和official docs still mentions coroutines under the context of the async APIs。因此,似乎一个简单的Coroutine和一个本地的Coroutine(一个与async … await …
)之间存在另一个差距。至少在现代的Python背景下,我们的Coroutine还不是真正的Coroutine。
让我们从文档中读取这些句子,解释了koude0 expression:
所有这些使生成器功能与Coroutines非常相似。它们产生多次,有一个以上的入口点,并且可以暂停执行。唯一的区别是,发电机函数无法控制执行后应在其产生后继续何处。控件始终传递到发电机的呼叫者。
因此,根据文档,听起来像是本机coroutines can 控制在将其控制在其他例程中的控制后应重新启动的位置(=函数)。
但是如何? koude78(or equivalently, koude79)表达式在内部工作,因此Coroutine对象可以在await (expression)
完成自己的执行后恢复其控制流。那么,这意味着await
摆脱了依靠呼叫者的控制的需求?
就我个人而言,我感到惊讶:Python仅提供界面,这取决于决定如何设计Coroutine控制流的程序员。因此,如果您阅读有关Coroutine对象的文档,则几乎没有任何技术解释来产生控制流。即使在PEP 492中,也只有关于 coroutine可以做什么的解释,但它 它如何做到。尽管yield
表现出非常具体的行为(保留其呼叫储藏和暂停),但只要满足其接口设计要求,await
就可以以任何方式实现。
但是,为什么我们为什么还讨论了到目前为止的发电机。这样的原因有两个:
-
本地旋ou的界面仍然与发电机的接口非常相似,因此了解发电机(或简单的Coroutines)的行为非常有用
-
deem demo stl asyncio library在其引擎盖下使用发电机的特征
因此,首先要了解发电机非常重要。根据我们的知识,我们将继续讨论本地的统治,最后是其余两篇文章中的异步图书馆。
结论
Coroutine概念首先是基于Python历史上的发电机实现的。现在,我们也知道,本地旋ou只是界面,这些接口是由诸如Asyncio或trio之类的库来实现的。我们将看到Coroutine对象如何与接下来两篇文章中图书馆内部的其他与异步相关的功能和谐相处。