【问题标题】:Copy a generator复制生成器
【发布时间】:2015-07-02 08:48:26
【问题描述】:

假设我有一个这样的生成器

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 语句创建的生成器。

【问题讨论】:

  • 这并不像听起来那么容易。如果生成器使用无法克隆的文件或网络流怎么办?虽然看看itertools.tee
  • 简短的回答是您不能在 Python 中克隆生成器。但至少在理论上可以添加自己的生成器克隆支持,如果您不关心实现之间的可移植性,这意味着您可以自己做出所有决定(文件共享、复制还是提升?是否共享闭包变量? 你是深拷贝还是浅拷贝本地人?)。如果您有兴趣这样做,您可以提出很多很好的问题,但只是“我该如何做整个事情”可能太宽泛了。
  • @user2357112:实际上,即使是这样,也是通过编译然后调用带有yield x+1 语句的隐藏函数来实现的……
  • @PyRulez:不,这实际上不是生成器。尽管它几乎可以在任何地方替代生成器,因此您可以将其称为“类似生成器的对象”,但它不会传递inspect.isgeneratorisinstance(g, types.GeneratorType),并且它没有gi_frame 属性。
  • @JoranBeasley:虽然他的玩具示例并不是将生成器用于协程的有用示例,但那里有很多很好的示例,它们都会遇到这个问题。 (如果没有,Guido 会拒绝添加 send 的 PEP,而不是成为它的合著者并确保它在 2.5 之前完成......)

标签: python python-2.7 clone generator coroutine


【解决方案1】:

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

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]

【讨论】:

  • 您不能使用发送设置a
  • 我确定,就在示例中。虽然公平地说,我错过了g.send(None)
  • @nelfin:我认为他的观点是,如果你想做a_ = yield b,它是行不通的,因为你已经把a_ 变成了一个闭包变量。 (在 Python 3 中,会有一个简单的修复:只需将 nonlocal a_ 添加到生成器的顶部。但在 2.7 中,这并不容易。)
  • 啊,好的,谢谢@abamert。在这种情况下,只需将a_ 包装成一个可变类型,如dict,然后在gen 本地重新绑定一个名称。
  • 在示例中,g.send(42) 被运行以将 42 分配给 a,但在此,它是一个函数调用。
【解决方案2】:

Python 不支持克隆生成器。

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


在幕后,生成器基本上只不过是堆栈帧的包装器。*

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

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

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

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


不幸的是,事实证明,Python 没有公开构造框架对象的方法。我认为你可以通过使用ctypes.pythonapi 调用PyFrame_New 来解决这个问题,但是第一个参数是PyThreadState——你绝对不能用Python 构造它,也不应该能够。因此,要完成这项工作,您必须:

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

我认为这仍然是可行的(我打算尝试一下;如果我有什么想法,我会更新我博客上的 Cloning generators 帖子),但这绝对不会是微不足道的——或者,当然,甚至可以远程携带。


还有几个小问题。

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

  • 异常状态作为类型/值/回溯 3 元组向 Python 公开,但在框架内还有一个对所属生成器的借用(非引用)引用(如果不是生成器框架,则为 NULL )。 (The source 解释了原因。)因此,您的框架构造函数无法重新计数生成器,或者您有一个循环因此泄漏,但它必须重新计数生成器,或者您有一个潜在的悬空指针,直到分配了框架到发电机。显而易见的答案似乎是在框架构造时将生成器设为 NULL,并让生成器构造函数执行与 self.gi_f.f_generator = self; Py_DECREF(self) 等效的操作。


* 它还保留了框架的代码对象和运行标志的副本,以便在生成器退出并释放框架后访问它们。

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

*** 当你编译一个函数定义时,编译器会列出所有局部变量,存储在co_varnames 中,并将每个变量引用转换为带有索引的LOAD_FAST/STORE_FAST 操作码进入co_varnames 作为它的参数。执行函数调用时,frame对象将堆栈指针存储在f_valuestack中,将len(co_varnames)*sizeof(PyObject *)压入堆栈,然后LOAD_FAST 0才访问*f_valuestack[0]。闭包更复杂;在对 SO 答案的评论中解释的有点太多了。

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

【讨论】:

  • 你应该推荐一个 PEP。
  • @PyRulez:PEP 需要一个好的基本原理部分,我没有很好的理由解释为什么要构造 framegenerator 对象(以及 cell 对象,如果你想要的话) 有必要的。此外,即使有理由,我也不认为这个想法是 PEP 准备好的。首先需要对 python-ideas 进行一些讨论。
  • @PyRulez:您当然可以开始讨论 python-ideas,但请注意,前 20 个响应将要求一个好的用例并向您展示玩具用例是最好使用不需要克隆生成器的解决方案,所以我会首先制定出你能想到的最小用例,它确实很有用,如果你想获得良好的吸收,确实确实需要克隆生成器。
  • 用例:类似于 Python 中的 Haskells parsec。通过克隆,您可以尝试不同的代码路径。列表单子的不确定性也是如此。
  • 我有一个用例。我正在尝试确定生成器表达式的 n 维生成器表达式的形状(例如 ((m*i + j for j in range(m)) for i in range(n)))以及展平列表上的迭代器。我认为不克隆生成器是不可能的。 Here's a GitHub gist that shows exactly what I mean.
【解决方案3】:

虽然从技术上讲不会返回生成器,但如果您不介意完全扩展您的序列:

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 强制您手动管理和关闭您的 ProcessPools,但也作为返回生成器的 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()

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-08-06
    • 2011-04-17
    • 1970-01-01
    • 2014-02-14
    • 2014-11-28
    • 2015-10-28
    • 2015-10-07
    相关资源
    最近更新 更多