Python 生成器和协程
我正在研究各种编程语言的协程和生成器。
我想知道是否有一种更干净的方法将通过生成器实现的两个协程组合在一起,而不是在调用者处产生任何被调用者产生的结果?
假设我们使用以下约定:除了最后一个之外的所有收益都返回 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
编辑:我建议使用 Greenlet。但如果您对纯 Python 方法感兴趣,请继续阅读。
这在 中解决PEP 342,但一开始有点难以理解。我将尝试简单地解释它是如何工作的。
首先,让我总结一下我认为您真正想要解决的问题是什么。
问题
您有一个调用其他生成器函数的生成器函数的调用堆栈。您真正想要的是能够从顶部的生成器产生收益,并将收益一直传播到堆栈中。
问题是 Python 不(在语言级别)支持真正的协程,只支持生成器。 (但是,它们是可以实现的。)真正的协程允许您停止整个函数调用堆栈并切换到不同的堆栈。生成器仅允许您停止单个函数。如果生成器 f() 想要yield,yield 语句必须位于 f() 中,而不是 f() 调用的另一个函数中。
我认为您现在使用的解决方案是执行类似于 Simon Stelling 的答案中的操作(即通过生成 g() 的所有结果,让 f() 调用 g() )。这是非常冗长和丑陋的,并且您正在寻找语法糖来包装该模式。请注意,这实际上会在每次让出时展开堆栈,然后再将其重新展开。
解决方案
有一个更好的方法来解决这个问题。基本上,您可以通过在“蹦床”系统之上运行生成器来实现协程。
要实现此目的,您需要遵循以下几种模式:
1. 当你想调用另一个协程时,yield它。
2. 不要返回一个值,而是产生它。
所以
变成
假设你在 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
becomes
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.
您在寻找这样的东西吗?
Are you looking for something like this?