Python生成器和协程

9 投票
2 回答
8896 浏览
提问于 2025-04-16 17:21

我正在学习各种编程语言中的协程和生成器。

我在想,有没有比直接把调用者需要的东西返回给调用者更简洁的方法来组合两个通过生成器实现的协程呢?

假设我们使用以下约定:除了最后一个返回结果外,所有的返回值都是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

在这种情况下,我希望通过一个叫做bind的东西(可能可以实现,也可能不能,这就是问题所在),协程B能够在A每次返回时都能得到A的返回值,直到A返回它的最终结果,然后这个结果被赋值给x,这样B就可以继续执行。

我怀疑实际的代码应该明确地去迭代A,所以:

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

这样写有点丑,而且容易出错……

附注:这是为了一个游戏,使用这种语言的用户是编写脚本的设计师(脚本=协程)。每个角色都有一个关联的脚本,还有很多子脚本是由主脚本调用的;比如,run_ship会多次调用reach_closest_enemy、fight_with_closest_enemy、flee_to_allies等等。所有这些子脚本需要像你描述的那样被调用;对开发者来说这没问题,但对设计师来说,写的代码越少越好!

2 个回答

18

编辑:我推荐使用 Greenlet。但如果你对纯Python的方法感兴趣,可以继续往下看。

这个问题在 PEP 342 中有提到,但一开始可能有点难理解。我会尽量简单地解释它是如何工作的。

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

问题

你有一堆生成器函数相互调用的情况。你真正想要的是能够从最上面的生成器中“产出”值,并让这个“产出”一直传递到下面的所有函数。

问题在于,Python 在语言层面上并不支持真正的协程,只有生成器。(不过,可以实现类似的功能。)真正的协程允许你暂停整个函数调用的堆栈,然后切换到另一个堆栈。而生成器只能暂停单个函数。如果一个生成器 f() 想要“产出”值,这个“产出”语句必须在 f() 中,而不能在 f() 调用的其他函数中。

我认为你现在使用的解决方案是像 Simon Stelling 的回答那样(也就是说,让 f() 通过“产出” g() 的所有结果来调用 g())。这种方式非常冗长且不美观,你希望有更简洁的语法来封装这个模式。需要注意的是,这样做实际上每次“产出”时都会展开堆栈,然后再重新组合起来。

解决方案

有一种更好的方法来解决这个问题。你可以通过在一个“蹦床”系统上运行生成器来实现协程。

要使这个方法有效,你需要遵循几个模式: 1. 当你想调用另一个协程时,进行“产出”。 2. 不要返回值,而是进行“产出”。

所以

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

变成

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

假设你在 f() 中。蹦床系统调用了 f()。当你“产出”一个生成器(比如 g())时,蹦床系统会代表你调用 g()。然后当 g() 完成“产出”值后,蹦床系统会重新启动 f()。这意味着你实际上并没有使用 Python 的堆栈;而是由蹦床系统来管理调用堆栈。

当你“产出”的不是生成器时,蹦床系统会把它当作返回值。它通过“产出”语句将这个值传回给调用的生成器(使用生成器的 .send() 方法)。

评论

这种系统在异步应用中非常重要和有用,比如使用 Tornado 或 Twisted 的应用。你可以在调用堆栈被阻塞时暂停它,去做其他事情,然后再回来继续执行最初的调用堆栈。

上述解决方案的缺点是,它要求你基本上将所有函数都写成生成器。使用真正的协程实现可能会更好 - 见下文。

替代方案

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

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

Python 3.3 应该会提供用于委托给子生成器的语法,见 PEP 380

2

你是在找像这样的东西吗?

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

   # continue, x contains value A yielded

撰写回答