复制发电机

时间:2015-04-23 22:56:02

标签: python python-2.7 clone generator coroutine

假设我有一个像这样的发电机

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

现在让我说我做

>>> g = gen()
>>> g.next()
>>> g.send(42)
a_ =  43
43

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

>>> newG = clonify(g)
>>> newG.send(7)
b = 7
a_ = 43
7

但我原来的g仍有效。

>>> g.send(11)
b = 11
a_ = 43
11

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

注意:itertools.tee无效,因为它无法处理发送。

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

3 个答案:

答案 0 :(得分:5)

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

从概念上讲,这应该是可实现的,至少对于CPython而言。但实际上,事实证明这很难。

在封面下,生成器基本上只是堆栈框架周围的包装。 *

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

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

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

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

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

  • 通过PyFrame_New
  • 敲击C结构,重现ctypes所做的一切
  • 通过敲击C结构手动构建假PyThreadState(仍然需要仔细阅读代码PyFrame_New以了解您必须伪造的内容)。

我认为这可能仍然可行(并且我打算使用它;如果我想出任何东西,我会更新我博客上的Cloning generators帖子),但它是绝对不会是微不足道的 - 当然,甚至是远程便携的。

还有一些小问题。

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

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

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

** generatorframe对内置隐藏,但它们可以types.GeneratorType types.FrameType使用。他们在inspect模块中有文档字符串,其属性描述等,就像函数和代码对象一样。

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

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

答案 1 :(得分:0)

总的来说,你不能。但是,如果你参与一些昂贵的操作,为什么不解除这个操作呢?创建一个发电机工厂?

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

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

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]

答案 2 :(得分:0)

虽然从技术上讲还没有返回生成器,但是如果您不介意完全扩展序列,则:

source = ( x**2 for x in range(10) )
source1, source2 = zip(*( (s,s) for s in source ))

>>> print( source1, type(source1) )
(0, 1, 4, 9, 16, 25, 36, 49, 64, 81) <class 'tuple'>

>>> print( source2, type(source2) )
(0, 1, 4, 9, 16, 25, 36, 49, 64, 81) <class 'tuple'>

如果您的函数很昂贵,请考虑使用joblibpathos.multiprocessing。 Joblib具有更简单的语法,并在后台处理池管理,但仅支持批处理。 Pathos迫使您手动管理和关闭您的ProcessPool,而且还需要使用pool.imap()pool.uimap()函数来返回生成器

from pathos.multiprocessing import ProcessPool

pool = ProcessPool(ncpus=os.cpu_count())
try:
    def expensive(x): return x**2
    source  = range(10)
    results = pool.imap(expensive, source)
    for result in results:
        print(result)
except KeyboardInterrupt: pass
except: pass
finally:
    pool.terminate()

从理论上讲,您可以将其设置为在单独的线程中运行,并传递两个队列对象,这些对象可以独立读取,并且可以保留此答案中建议的类似生成器的行为: