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在函数中放置语句而创建的生成器.
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构造新的单元格对象.
一般来说,你不能。但是,如果您对某些昂贵的操作进行参数化,为什么不取消该操作,创建一个发电机工厂呢?
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)