复制发电机

PyR*_*lez 4 python clone generator coroutine python-2.7

假设我有一台这样的发电机

def gen():
    a = yield "Hello World"
    a_ = a + 1 #Imagine that on my computer "+ 1" is an expensive operation
    print "a_ = ", a_
    b = yield a_
    print "b =", b
    print "a_ =", a_
    yield b
Run Code Online (Sandbox Code Playgroud)

现在让我说我做

>>> g = gen()
>>> g.next()
>>> g.send(42)
a_ =  43
43
Run Code Online (Sandbox Code Playgroud)

现在我们计算了a_.现在我想像这样克隆我的发电机.

>>> newG = clonify(g)
>>> newG.send(7)
b = 7
a_ = 43
7
Run Code Online (Sandbox Code Playgroud)

但我的原作g仍然有效.

>>> g.send(11)
b = 11
a_ = 43
11
Run Code Online (Sandbox Code Playgroud)

具体来说,clonify获取生成器的状态,并将其复制.我可以将我的发电机重置为旧发电机,但这需要计算a_.另请注意,我不想广泛修改生成器.理想情况下,我可以从库中获取生成器对象clonify.

注意:itertools.tee不起作用,因为它不处理发送.

注意:我只关心通过yield在函数中放置语句而创建的生成器.

aba*_*ert 7

Python对克隆生成器没有任何支持.

从概念上讲,这应该是可实现的,至少对于CPython而言.但实际上,结果却非常困难.


在封面下,发电机基本上只是堆叠框架周围的包装物.*

框架对象本质上只是一个代码对象,一个指令指针(该代码对象的索引),内置/全局/本地环境,异常状态,以及一些标志和调试信息.

这两种类型都暴露在Python级别,**以及它们所需的所有位.所以,它应该只是一个问题:

  • 创建一个框架对象g.gi_frame,但是使用本地副本而不是原始本地副本.(所有用户级问题都归结为是否需要浅拷贝,深拷贝,或上述其中一个以及递归克隆生成器.)
  • 从新的框架对象(及其代码和运行标志)中创建一个生成器对象.

并且没有明显的实际原因,不应该从其位构造一个框架对象,就像它对于代码对象或大多数其他隐藏的内置类型一样.


不幸的是,事实证明,Python没有公开构造框架对象的方法.我认为你可以通过使用ctypes.pythonapi来调用它PyFrame_New,但第一个参数是PyThreadState- 你绝对不能用Python构造,而且不应该.所以,要做到这一点,你要么必须:

  • PyFrame_New通过敲击C结构来重现一切ctypes,或者
  • PyThreadState通过敲击C结构手动构建假(这仍然需要PyFrame_New仔细阅读代码才能知道你必须伪造什么).

我认为这可能仍然可行(我计划玩它;如果我想出任何东西,我会在我的博客上更新克隆生成器帖子),但它绝对不会是微不足道的 - 当然,甚至远程便携.


还有一些小问题.

  • 本地作为dict暴露给Python(无论你是locals()为自己调用,还是g.gi_frame.f_locals为要克隆的生成器访问).在幕后,本地人实际上存储在C堆栈上.***你可以通过使用ctypes.pythonapi来调用PyFrame_LocalsToFast和解决这个问题PyFrame_FastToLocals.但是dict只包含值,而不是单元格对象,因此执行此shuffle会将所有非局部变量转换为克隆中的局部变量.****

  • 异常状态作为类型/值/跟踪3元组暴露给Python,但是在框架内还有对所拥有的生成器的借用(非引用)引用(如果它不是生成器框,则为NULL).(来源解释了原因.)所以,你的帧构造函数不能重新计算生成器,或者你有一个循环因此泄漏,但它必须重新计算生成器或你有一个潜在的悬空指针,直到帧被分配给发电机.显而易见的答案似乎是在帧构造中将生成器保留为NULL,并且让生成器构造函数执行相当于self.gi_f.f_generator = self; Py_DECREF(self).


*它还保留了帧的代码对象和运行标志的副本,因此可以在生成器退出并丢弃帧之后访问它们.

**generator并且frame隐藏在内置物中,但它们可以作为.它们有文档字符串,模块中属性的描述等,就像函数和代码对象一样.types.GeneratorType types.FrameTypeinspect

***编译函数定义时,编译器会生成所有本地存储的列表co_varnames,并将每个变量引用转换为LOAD_FAST/ STORE_FASTopcode,并将索引co_varnames作为参数.当执行函数调用时,帧对象将堆栈指针存储在其中f_valuestack,推len(co_varnames)*sizeof(PyObject *)入堆栈,然后LOAD_FAST 0只是访问*f_valuestack[0].关闭更复杂; 在SO答案的评论中有点太多无法解释.

****我假设您希望克隆共享原始的闭包引用.如果你希望递归地克隆堆栈中的所有帧以获得一组新的绑定引用,那就会增加另一个问题:无法从Python构造新的单元格对象.

  • 你应该建议一个 PEP。 (2认同)
  • @PyRulez:PEP 需要一个好的基本原理部分,而我没有很好的基本原理来解释为什么需要构造“frame”和“generator”对象(以及“cell”对象,如果你想要的话)。此外,即使有理由,我也不认为这个想法适合 PEP;首先需要对 python 的想法进行一些讨论。 (2认同)
  • 用例:类似于 Python 中的 Haskells parsec。通过克隆,您可以尝试代码的不同路径。列表单子也具有非确定性。 (2认同)

nel*_*fin 3

一般来说,你不能。但是,如果您对某些昂贵的操作进行参数化,为什么不取消该操作,创建一个发电机工厂呢?

def make_gen(a):
    a_ = [a + 1]  # Perform expensive calculation
    def gen(a_=a_):
        while True:
            print "a_ = ", a_
            a_[0] = yield a_[0]
    return gen
Run Code Online (Sandbox Code Playgroud)

然后,您可以根据返回的对象创建任意数量的生成器:

gen = make_gen(42)
g = gen()
g.send(None)
# a_ = [43]
g.send(7)
# a_ = [7]
new_g = gen()
new_g.send(None)
# a_ = [7]
Run Code Online (Sandbox Code Playgroud)

  • @nelfin:我认为他的观点是,如果你想做`a_=yield b`,它是行不通的,因为你已经把`a_`变成了一个闭包变量。(在 Python 3 中,会有一个简单的修复:只需将 `nonlocal a_` 添加到生成器的顶部。但在 2.7 中,这就没那么容易了。) (2认同)