Python 生成器和协程

发布于 2024-11-06 11:37:56 字数 838 浏览 3 评论 0原文

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

我想知道是否有一种更干净的方法将通过生成器实现的两个协程组合在一起,而不是在调用者处产生任何被调用者产生的结果?

假设我们使用以下约定:除了最后一个之外的所有收益都返回 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 so:

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等。所有这些子脚本都需要按照您上面描述的方式调用;对于开发人员来说这不是问题,但对于设计师来说,他们编写的代码越少越好!

I am studying coroutines and generators in various programming languages.

I was wondering if there is a cleaner way to combine together two coroutines implemented via generators than yielding back at the caller whatever the callee yields?

Let's say that we are using the following convention: all yields apart from the last one return null, while the last one returns the result of the coroutine. So, for example, we could have a coroutine that invokes another:

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

in this case I wish that through bind (which may or may not be implementable, that's the question) the coroutine B yields whenever A yields until A returns its final result, which is then assigned to x allowing B to continue.

I suspect that the actual code should explicitly iterate A so:

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

which is a tad ugly and error prone...

PS: it's for a game where the users of the language will be the designers who write scripts (script = coroutine). Each character has an associated script, and there are many sub-scripts which are invoked by the main script; consider that, for example, run_ship invokes many times reach_closest_enemy, fight_with_closest_enemy, flee_to_allies, and so on. All these sub-scripts need to be invoked the way you describe above; for a developer this is not a problem, but for a designer the less code they have to write the better!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

素罗衫 2024-11-13 11:37:56

编辑:我建议使用 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 堆栈;而是在使用 Python 堆栈。蹦床系统改为管理调用堆栈。

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

注释

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

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

Python 的协程

有多种实现,请参阅:http://en.wikipedia.org/wiki /Coroutine#Implementations_for_Python

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

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

Edit: I recommend using Greenlet. But if you're interested in a pure Python approach, read on.

This is addressed in PEP 342, but it's somewhat tough to understand at first. I'll try to explain simply how it works.

First, let me sum up what I think is the problem you're really trying to solve.

Problem

You have a callstack of generator functions calling other generator functions. What you really want is to be able to yield from the generator at the top, and have the yield propagate all the way down the stack.

The problem is that Python does not (at a language level) support real coroutines, only generators. (But, they can be implemented.) Real coroutines allow you to halt an entire stack of function calls and switch to a different stack. Generators only allow you to halt a single function. If a generator f() wants to yield, the yield statement has to be in f(), not in another function that f() calls.

The solution that I think you're using now, is to do something like in Simon Stelling's answer (i.e. have f() call g() by yielding all of g()'s results). This is very verbose and ugly, and you're looking for syntax sugar to wrap up that pattern. Note that this essentially unwinds the stack every time you yield, and then winds it back up again afterwards.

Solution

There is a better way to solve this problem. You basically implement coroutines by running your generators on top of a "trampoline" system.

To make this work, you need to follow a couple patterns:
1. When you want to call another coroutine, yield it.
2. Instead of returning a value, yield it.

so

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

becomes

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

Say you're in f(). The trampoline system called f(). When you yield a generator (say g()), the trampoline system calls g() on your behalf. Then when g() has finished yielding values, the trampoline system restarts f(). This means that you're not actually using the Python stack; the trampoline system manages a callstack instead.

When you yield something other than a generator, the trampoline system treats it as a return value. It passes that value back to the caller generator through the yield statement (using .send() method of generators).

Comments

This kind of system is extremely important and useful in asynchronous applications, like those using Tornado or Twisted. You can halt an entire callstack when it's blocked, go do something else, and then come back and continue execution of the first callstack where it left off.

The drawback of the above solution is that it requires you to write essentially all your functions as generators. It may be better to use an implementation of true coroutines for Python - see below.

Alternatives

There are several implementations of coroutines for Python, see: http://en.wikipedia.org/wiki/Coroutine#Implementations_for_Python

Greenlet is an excellent choice. It is a Python module that modifies the CPython interpreter to allow true coroutines by swapping out the callstack.

Python 3.3 should provide syntax for delegating to a subgenerator, see PEP 380.

云仙小弟 2024-11-13 11:37:56

您在寻找这样的东西吗?

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

   # continue, x contains value A yielded

Are you looking for something like this?

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

   # continue, x contains value A yielded
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文