【问题标题】:Python generators and coroutinesPython 生成器和协程
【发布时间】:2011-08-22 08:41:27
【问题描述】:

我正在研究各种编程语言的协程和生成器。

我想知道是否有一种更简洁的方法可以将通过生成器实现的两个协程组合在一起,而不是在被调用者产生的任何情况下返回给调用者?

假设我们使用以下约定:除最后一个之外的所有 yield 返回 null,而最后一个返回协程的结果。因此,例如,我们可以有一个调用另一个协程:

def A():
  # yield until a certain condition is met
  yield result

def B():
  # do something that may or may not yield
  x = bind(A())
  # ...
  return result

在这种情况下,我希望通过绑定(这可能是可实现的,也可能是不可实现的,这是个问题)协程 B 在 A 产生时产生,直到 A 返回其最终结果,然后将其分配给 x 允许 B 继续。

我怀疑实际的代码应该显式地迭代 A 这样:

def B():
  # do something that may or may not yield
  for x in A(): ()
  # ...
  return result

这有点丑陋且容易出错......

PS:这是一个游戏,语言的用户将是编写脚本的设计师(脚本=协程)。每个字符都有一个关联的脚本,并且有许多子脚本被主脚本调用;考虑一下,例如,run_ship 多次调用reach_closest_enemy、fight_with_closest_enemy、flee_to_allies 等等。所有这些子脚本都需要按照您上面描述的方式调用;对于开发人员来说这不是问题,但对于设计师来说,他们必须编写的代码越少越好!

【问题讨论】:

  • 我认为这些示例清楚地表明了惯用语。由于我无法理解这些示例有什么问题,您能否说明您发现哪些示例不清楚?哪些例子令人困惑?您能否更具体地说明所有那些无法显示惯用 Python 的示例?
  • 我已经准确地阅读了这些文章,PEP-342 让我有些困惑:它是当前在 Python 中工作的一些实际扩展吗? Trampoline 类是否是该语言标准库的一部分?顺便说一句,我的问题非常准确,它是关于通过协程控制的惯用方式。我可以阅读大量的方法来做到这一点,这一事实确实无济于事。你的狡猾也不...
  • 惯用语是关于执行某些功能的“标准”方式;迭代嵌套协程的结果绝对没有错,但是在编程语言文献中有一些示例,其中 yield 会自动爬下调用堆栈,因此您不需要在每个调用者处重新屈服,因此我很好奇如果这种模式是否被 Python 中的语法糖所覆盖!
  • @Giuseppe Maggiore:“产生自动爬下调用堆栈的编程语言”这听起来不像是同一个问题。您是在要求惯用的 Python(如许多示例所示),还是要求在 Python 示例中未显示但在其他语言中显示的其他功能?恐怕我根本无法理解你的问题。你能澄清一下你真正在寻找什么吗?
  • 您可能想看看“惯用语”是什么意思,因为我感觉这就是我们遇到问题的地方 (thefreedictionary.com/idiomatic)。我知道我可以在 Python 中构建协程。我知道如何使用生成器来做到这一点。我想知道(A)是否有一种公认的“正确方法”可以这样做,它可能使用一些语法糖或其他技术,或者(B)如果没有建议并且任何可行的方法都是可以接受的。 (A) 是我所说的惯用语。相比之下,使用具有大量可变引用的函数式语言编写不会是惯用的。干杯

标签: python generator coroutine


【解决方案1】:

你在寻找这样的东西吗?

def B():
   for x in A():
     if x is None:
       yield
     else:
       break

   # continue, x contains value A yielded

【讨论】:

  • 是的,这就是我正在做的。我的问题是这是否是惯用的方式,或者是否有一些句法结构能够隐藏这种在我的应用程序中经常出现的模式。
  • @Guiseppe Maggiore:我不知道有任何这样的结构。但是,您经常需要这种模式似乎很奇怪……我想不出许多有效的用例。如果您提供更多上下文信息,也许我们可以提出一个更优雅的替代解决方案?
  • 这是一个游戏,语言的用户将是编写脚本的设计师(脚本=协程)。每个字符都有一个关联的脚本,并且有许多子脚本被主脚本调用;考虑一下,例如,run_ship 多次调用reach_closest_enemy、fight_with_closest_enemy、flee_to_allies 等等。所有这些子脚本都需要按照您上面描述的方式调用;对于开发人员来说这不是问题,但对于设计师来说,他们必须编写的代码越少越好!
  • @Guiseppe Maggiore:我建议您将最后一条评论添加到问题中,以便其他人也有机会回答它......
【解决方案2】:

编辑:我推荐使用Greenlet。但如果您对纯 Python 方法感兴趣,请继续阅读。

这在PEP 342 中得到解决,但起初有点难以理解。我将尝试简单地解释它是如何工作的。

首先,让我总结一下我认为你真正想要解决的问题。

问题

您有一个调用其他生成器函数的生成器函数的调用堆栈。您真正想要的是能够从顶部的生成器中产生收益,并让收益一直沿堆栈向下传播。

问题在于 Python 不(在语言层面)支持真正的协程,只支持生成器。 (但是,它们可以实现。)真正的协程允许您暂停整个函数调用堆栈并切换到不同的堆栈。生成器仅允许您停止单个功能。如果生成器 f() 想要 yield,则 yield 语句必须在 f() 中,而不是在 f() 调用的另一个函数中。

我认为您现在正在使用的解决方案是在 Simon Stelling 的回答中执行类似操作(即通过产生 g() 的所有结果让 f() 调用 g())。这是非常冗长和丑陋的,您正在寻找语法糖来包装该模式。请注意,这实际上是在每次让出时展开堆栈,然后再将其重新回卷。

解决方案

有更好的方法来解决这个问题。您基本上是通过在“蹦床”系统上运行生成器来实现协同程序。

要完成这项工作,您需要遵循以下几种模式: 1.当你想调用另一个协程时,yield它。 2. 不是返回一个值,而是产生它。

所以

def f():
    result = g()
    # …
    return return_value

变成

def f():
    result = yield g()
    # …
    yield return_value

假设你在 f() 中。蹦床系统称为 f()。当您生成生成器(例如 g())时,蹦床系统会代表您调用 g()。然后当 g() 完成产生值时,蹦床系统重新启动 f()。这意味着您实际上并没有使用 Python 堆栈。蹦床系统改为管理调用堆栈。

当您生成生成器以外的其他东西时,蹦床系统会将其视为返回值。它通过 yield 语句将该值传递回调用者生成器(使用生成器的 .send() 方法)。

评论

这种系统在异步应用程序中非常重要和有用,例如使用 Tornado 或 Twisted 的应用程序。您可以在整个调用堆栈被阻塞时停止它,然后去做其他事情,然后返回并继续执行第一个调用堆栈中断的地方。

上述解决方案的缺点是它需要您将所有函数编写为生成器。使用 Python 的真正协程实现可能会更好 - 见下文。

替代品

Python 的协程有多种实现,参见:http://en.wikipedia.org/wiki/Coroutine#Implementations_for_Python

Greenlet 是一个很好的选择。它是一个 Python 模块,通过交换调用堆栈来修改 CPython 解释器以允许真正的协程。

Python 3.3 应该提供委派给子生成器的语法,请参阅PEP 380

【讨论】:

  • 非常有用且清晰的答案,谢谢!但是,当您说标准 Python 协程本质上需要将所有函数编写为生成器时,您是指仅一级函数还是真正的所有函数?正如你上面所说的,当产生不是生成器的东西时,蹦床系统仍然有效,所以理论上我们可以在第一层函数处产生任何其他可能或可能不是生成器本身的函数。我说的对吗?
  • 蹦床系统和产量之间的所有“功能”都必须写成生成器。您可以正常调用常规函数,但是您不能有效地从该函数或它调用的任何函数中“屈服”。这有意义/回答你的问题吗?
  • 我强烈推荐使用Greenlet——它是 Python 协程的真正实现,您不必使用我所描述的任何这些模式。蹦床的东西是为那些对如何在纯 Python 中做到这一点感兴趣的人准备的。
  • 对于任何在 2015 年或之后阅读本文的人,新语法是“yield from” (PEP 380),它允许 Python >3.3 中的真正协程。
猜你喜欢
  • 2017-09-02
  • 2015-10-31
  • 2013-08-02
  • 2015-12-11
  • 2019-07-09
  • 2021-04-30
  • 2023-03-05
  • 2016-03-31
相关资源
最近更新 更多