Python生成器的多个客户端?

时间:2012-03-19 12:10:50

标签: python generator coroutine

作为this question的后续内容,我试图绕过range(int(1e8))使用生成器xrange(int(1e8))举例说明的列表构建。其中xrange只是生成一长串值的过程的示例。 (请假设它不能轻易复制。)还有一些背景知识,我有一长串的时间戳/值对,我想对它进行一些处理(有时间序列)。我试图避免将这些内容整体记录到内存中,因为这是很多数据。

我认为如果我能同时将多个处理单元应用于我的生成器生成的数据流,那将会很酷。第一个想法是使用itertools.tee(),例如:

from itertools import tee
g1,g2 = tee(xrange(int(1e8)),2)
sum(g1), sum(g2)

但后来我发现只有第一个sum()会使用生成器,而tee()会在内部再次构建list(我想避免使用它。)。

所以我想,我需要一个异步解决方案,即允许每个sum()更新每个生成器步骤的解决方案。 想到的事情

但是我之前没有真正使用过,部分我甚至无法判断这些方法是否有效,或者是否有效/高效/高效。

从这一点来说,我很乐意感谢观众的任何建议!


更新

我想避开callback based solution,因为它显着降低了性能(这是它目前的实现方式)。我在下面添加了一些分析(如果测试不客观,请添加评论):

class SinkA:
  def __init__(self, src):
    for i in src: pass

class SinkB:
  def f(self,i):
    pass

class Source:
  def __iter__(self):
    for i in xrange(int(1e4)):
      yield i

def t1():
  src = Source()
  snk = SinkA(src)

def t2():
  src = Source()
  snk = SinkB()
  for i in src: snk.f(i)

if __name__ == "__main__":
    from timeit import Timer
    n = 1000
    t = Timer("t1()", "from __main__ import t1, t2, SinkA, SinkB, Source")
    print "%.2f usec/pass" % (1000000 * t.timeit(number=n)/n) # 612.11 usec/pass
    t = Timer("t2()", "from __main__ import t1, t2, SinkA, SinkB, Source")
    print "%.2f usec/pass" % (1000000 * t.timeit(number=n)/n) # 1933.39 usec/pass

更新2

我还能说什么?我有这个基于回调的解决方案,看起来效率很低。基于生成器的方法看起来很有前途,但我对这种编程的经验太少,特别是当它涉及更复杂的事物如协同程序或扭曲的库时。 总而言之,我有一个生成大量数据的流程的多个消费者,我发现了一些潜在的方法。现在我正在寻找有经验的用户可能已经完成类似任务的合格声明。处理哪种方法可能合适的方式,方法如何相互关联。或者我可能还有其他什么方法。

5 个答案:

答案 0 :(得分:5)

作为一种通用方法,我会用回调替换生成器的拉模型,并且可能包装生成器,如下所示:

def walk(gen, callbacks):
    for item in gen:
        for f in callbacks:
            f(item)

如果您的处理器位于不同的线程中并且您希望它们在等待时阻塞,则可以将Queue.put(或任何等效的)注册为每个处理器的回调,并独立轮询这些队列。如果需要,这将允许您使用广播和工作池模型。

修改

另一种解决方案是使用coroutines

def source(self, *dests):
    for i in xrange(int(1e4)):
        for dest in dests:
            dest.send(i)

def sink():
    while True:
        i = yield

def t3():
    snk = sink()
    snk.next() # activate the coroutine
    source(snk)

if __name__ == '__main__':

    from timeit import Timer
    n = 1000
    t = Timer("t3()", "from __main__ import source, sink, t3")
    print "%.2f usec/pass" % (1000000 * t.timeit(number=n)/n) # 872.99 usec/pass

看起来足够快。基本上,协程是倒置的发生器,你从发电机拉出来,推到协程。

答案 1 :(得分:1)

您没有真正解决这个问题,但是您是否希望每个消费者看到完全相同的数据(在这种情况下tee可能是最佳解决方案),或者不是?

如果没有,那么您可以让每个消费者从一个生成器对象中读取。

如果您确实希望它们获得完全相同的数据,请尝试tee(使用更多内存)与两个生成器(更多IO),并查看哪个更快。

关于你的时间,你的数据显示的只是多个函数调用的开销,并且你的一个方法避免了中间函数调用。

如果你想提高性能,试试在PyPy上运行它,它有一个热点优化JIT。

答案 2 :(得分:1)

由于发电机内存便宜,为什么不简单地使用两个独立的发电机?

g1 = xrange(int(1e8))
g2 = xrange(int(1e8))
sum(g1), sum(g2)

答案 3 :(得分:0)

我建议您查看如何使用coroutines实现此功能,更具体地说,broadcast example

答案 4 :(得分:0)

与测试共享python生成器的解决方案:

https://gist.github.com/earonesty/cafa4626a2def6766acf5098331157b3

使用示例:

def mygen():
       yield from [1,2,3]

m1 = Muxer(mygen)
m2 = Muxer(mygen)

consume1(m1)
consume2(m2)

muxer.py的代码:

import queue
from threading import Lock
from collections import namedtuple

class Muxer():
    Entry = namedtuple('Entry', 'genref listeners, lock')

    already = {}
    top_lock = Lock()

    def __init__(self, func, restart=False):
        self.restart = restart
        self.func = func
        self.queue = queue.Queue()

        with self.top_lock:
            if func not in self.already:
                self.already[func] = self.Entry([func()], [], Lock())
            ent = self.already[func]

        self.genref = ent.genref
        self.lock = ent.lock
        self.listeners = ent.listeners

        self.listeners.append(self)

    def __iter__(self):
        return self

    def __next__(self):
        try:
            e = self.queue.get_nowait()
        except queue.Empty:
            with self.lock:
                try:
                    e = self.queue.get_nowait()
                except queue.Empty:
                    try:
                        e = next(self.genref[0])
                        for other in self.listeners:
                            if not other is self:
                                other.queue.put(e)
                    except StopIteration:
                        if self.restart:
                            self.genref[0] = self.func()
                        raise
        return e

    def __del__(self):
        with self.top_lock:
            try:
                self.listeners.remove(self)
            except ValueError:
                pass
            if not self.listeners and self.func in self.already:
                del self.already[self.func]