Python 中的协程:最佳实践

Coroutines in Python: Best Practices

我想知道在 Python 中编写协程的最佳实践是什么 3. 我正在开发应该接受一些输入的基本方法(使用 .send() 方法),对该输入执行计算,然后yield输出。

我发现的第一种方法基本上是执行以下操作:

def coroutine(func):
  data = yield
  while 1:
    data = yield func(data)

这似乎行得通,但循环中的那条线让我费解了。它似乎首先产生一个函数,然后然后接受输入并在恢复后执行分配。这对我来说完全不直观。

我正在寻找的另一种方法是:

def coroutine():
  while 1:
    data = yield
    [ do stuff with data here ... ]
    yield result

这段代码对我来说更容易理解,它还让我可以将代码直接放入生成器中,而不是传递一个函数。但是使用起来很烦人。每次实际调用生成器(如 "gen.send(2)")后都必须跟一个 "gen.send(None)" 以使生成器前进到下一个产量。

在我看来,这里的问题源于 "yield" 关键字用于两种不同的事物:return 语句和输入语句。

如果可能的话,我想要一种方法让我接受输入,对该输入进行计算,然后产生输出,而不必像第一种方法那样传入函数和使用单行代码,也不必发送无关的值与第二种方法一样。我该怎么做?


请注意:实际上,我会发送多个值。因此,具有无关 "g.send(None)" 语句的问题变得更糟。

您可以像在第一个示例中那样做。你只需要 "do stuff with data" 在循环中。这是一个例子:

def coroutine():
  data = yield
  while True:
    print("I am doing stuff with data now")
    data = data * 2
    data = yield data

你可以这样使用它:

>>> co = coroutine()
>>> next(co)
>>> co.send(1)
I am doing stuff with data now
2
>>> co.send(88)
I am doing stuff with data now
176

你说得对,yield 扮演着双重角色,既产生结果又接受随后通过 send 传入的值。 (同样,send 起着双重和互补的作用,因为每个 send 调用 returns 生成器产生的值。)注意那里的顺序:当你有一个 yield表达式,它首先产生值out,然后yield表达式的值变成afterwards[=36中的sent =].

这可能看起来 "backwards",但您可以通过循环执行它来使其成为 "forwards",就像您基本上已经这样做的那样。这个想法是你首先产生一些初始值(可能是一个无意义的值)。这是必要的,因为在生成值之前不能使用 send(因为没有 yield 表达式来评估发送的值)。然后,每次使用 yield 时,都会给出 "current" 值,同时接受用于计算 "next" 值的输入。

正如我在评论中提到的,从您的示例中并不清楚您为什么要使用生成器。在许多情况下,只需编写一个 class 就可以达到类似的效果,它有自己的传入和传出方法,如果你写 class,你可以使 API 随便你。如果选择使用生成器,则必须接受sendyield的双重input/output角色。如果您不喜欢那样,请不要使用生成器(或者,如果您需要它们提供的暂停功能状态,您可以使用它们,但用 class 将它们包装起来,将发送与产生分开)。

对 BrenBarn 的回答添加一个重要的说明:“当你有一个 yield 表达式时,它首先将值输出,然后 yield 表达式的值变成之后发送的任何值。”并不完全准确,只发生在他给出的例子中,因为循环中使用了相同的产量。实际发生的是首先进行 yield 分配(在程序暂停的 yield 处),然后继续执行下一个 yield,return 是它的结果。

当您使用 send() 方法时,它将在暂停执行的产量处进行赋值(但不是 return 那个产量的结果),然后继续到下一个产量点一个值将被 returned 并且执行将暂停。这在以下图形和示例代码中进行了演示。下面是一个用于同步硬件系统建模和验证的设计模式,创建了最多可以接受 M 个输入并在每次迭代中提供 N 个输出的设计组件,并演示了我描述得很好的操作:

此代码使用Python3.8 demonstrates/confirms上述操作:

def GenFunc():
    x = 'a'
    in1 = yield x
    y = 'b'
    print(f"After first yield: {in1=}, {y=}")
    in2 = yield y
    z = 'c'
    print(f"After second yield: {in1=}, {in2=}")
    in3 = yield z
    print(f"After third yield: {in1=}, {in2=}, {in3=}")

其中执行如下:

>>> mygen = GenFunc()
>>> next(mygen)
Out: 'a'
>>> mygen.send(25)
After first yield: in1=25, y='b'
Out: 'b'
>>> mygen.send(15)
After second yield: in1=25, in2=15
Out: 'c'
>>> mygen.send(45)
After third yield: in1=25, in2=15, in3=45
-----------------------------
StopInteration Error

这是一个额外的例子,显示了在循环中使用单个 yield 的相同行为:

def GenFunc(n):
    x = 0
    while True:
    n += 1
    x = yield n,x 
    x += 1
    print(n,x)
    x += 1

其中执行如下:

>>> mygen = GenFunc(10)
>>> next(mygen)
Out: (11, 0)
>>> mygen.send(5)
11 6
Out: (12, 7)