在Python中创建反应式迭代器的策略是什么?

时间:2014-11-22 18:54:24

标签: python reactive-programming

我一直在阅读Javascript世界中functional reactive programming的许多令人兴奋的发展。我也被Python的iterator protocol迷住了。我知道迭代器可以用来构建协同例程,现在我想知道,构建被动迭代器的方法是什么,我们称之为“流”,这样迭代流将阻塞,直到一个新值传递给流?

以下是我希望能够做到的一个例子:

my_stream = Stream()

for x in my_stream: # <-- this "blocks" the co-routine if my_stream is empty
    do_something_to(x)

# ... meanwhile, elsewhere, in another co-routine or whatever...
my_stream.send('foo') # <-- this advances any on-going iterations on my_stream

传统上,当迭代器完成时,它将raise StopIteration并且for循环将结束。相反,我希望for循环(即下一次调用stream.next())“阻塞”并将控制权交给另一个执行流程,无论是greenlet还是协程或其他任何流程。

我认为我要做的是避免信号/回调模式,因为回调在Python中是如此尴尬,除非它们适合lambda。这就是我所说的“反应迭代器” - 流控制被反转,for循环的主体(或任何在流上迭代的东西)变为反应而不是主动,实质上是一个内联块回调,只要项目触发进入流。

那么,以前做过吗?如果没有,那么什么样的模式/库/什么可以让它工作? GEVENT?龙卷风的IOLoop? greenlets?

3 个答案:

答案 0 :(得分:2)

这看起来很像使用迭代器协议包装线程和排队。

import threading
import random
import Queue
import time

class Supplier(threading.Thread):
    def __init__(self, q):
        self.queue = q
        threading.Thread.__init__(self)

    #This is the 'coroutine', it puts stuff in the queue at random
    #intervals up to three seconds apart
    def run(self):
        for i in range(10):
            self.queue.put(i)
            time.sleep(random.random()*3)
        self.queue.put(StopIteration())

class Consumer(object):
    def __init__(self, q):
        self.queue = q

    def __iter__(self):
        return self

    def next(self):
        #The call to Queue.get below blocks indefinitely unless we specify a timeout,
        item = self.queue.get()
        self.queue.task_done()
        if isinstance(item, StopIteration):
            raise StopIteration
        else:
            return item

Q = Queue.Queue()
S = Supplier(Q)
C = Consumer(Q)

S.start()

for item in C:
    print item

编辑:解决@David Eyk的评论

您可以使用greenlet,stackless或任何其他轻量级并发编程库/系统重新实现我的示例,示例的基本原理将保持不变。流(以FRP术语表示)是一个队列,所有调度,锁定和同步都意味着,无论它是如何实现的。公平地说,队列具有缓冲流插入的额外能力,这可能是不合需要的,在这种情况下,将队列的最大长度设置为1将导致流插入(放入队列)阻塞。协程是一个并发执行的代码块,无论它是一个线程,还是一个单独的执行堆栈。唯一的区别是切换发生时,确定性地或处理器控制。但我要提醒一点,并发代码块之间确定性切换和流控制的想法是乐观。在FRP术语中,流本质上是异步的,主要是因为它们依赖于中断驱动的 IO作为输入源,这意味着它们不像您想象的那样具有确定性。这对于从文件读取的流来说甚至是正确的,因为寻求,总线拥塞等导致IO速度的变化等。明确地(即确定地)将控制流切换到另一个协程的想法在功能上与同步相同在线程中的某个点。执行堆栈被切换,程序指针移动。当然,有一些轻量级和重量级的方法可以做到这一点。正如其他地方所提到的,Consumer类可以简单地重写为生成器,它是一个实现它自己的显式堆栈的对象,并提供一种产生控制的显式方法,即确定性微线程或协同程序。事实上,上面例子中的线程是一个辅助概念。使用send还将删除对显式队列的要求。然后,如果供应商是处理中断并将它们转换为事件对象并将它们放入事件队列(即流)的事件处理器,我们可以从示例中删除线程(至少是明确的),但它会变得复杂得多。关键是,轻量级与否,无论你是否看到它,在FRP中都会发生某处

编辑2:有必要对队列进行更实际的解释

尝试使用队列将消费者重新设置为生成器,实际上是微不足道的

def Consumer():
    while True:
        item = self.queue.get()
        self.queue.task_done()
        if isinstance(item, StopIteration):
            raise StopIteration
        else:
            yield item

但是,一旦迭代器参与,或者更具体地说是在迭代器上循环,删除队列以支持使用send和yield表达式并不是那么简单。 send方法与生成器内的yield表达式一起使用,例如

def Consumer(supplied_item = None):
    ignore = yield ignore #Postion A: ignores_initiating None
    while True:
        supplied_item = yield #Position B
        if supplied_item is not False:
            yield supplied_item #Position C
        else:
            raise StopIteration()

问题是在生成器上调用next,就像for循环一样,与使用None作为参数调用send基本相同。由于供应商和消费循环之间没有同步,因此消费者生成器可以接收以下输入序列

  • 无(需要开始进行;在A位接收)
  • 1(来自供应商;收到位置B)
  • 无(来自下一个for循环调用;位于C位置)
  • 无(来自下一个for循环调用,因为供应商仍然在睡觉;在B位接收)
  • 无(来自下一个for循环调用因为供应商仍处于休眠状态;在C位接收)

这意味着生成器产生for循环:[1,None,None,None,...]。根据供应商再次启动的时间,发送到位置B或C,for循环可能永远不会看到2,3等。所以,事实证明,如果你想使用你的协程作为迭代器,你显然有使用队列(或其他一些同步方法)来避免这个问题。如果有办法指定您希望将转换为的位置,例如只有在从供应商处调用时才会产生,而不是for循环,否则阻塞。

答案 1 :(得分:2)

你一定是在谈论generators。从生成器读取代码(=调用其next())阻塞,直到生成器yield为止。随着时间的推移(PEP 342 - 2.5PEP 380 - 3.3)有一些增强功能可以简化将生成器用作协同程序。

P P 380 380的冠军Greg Ewing在https://mail.python.org/pipermail/python-ideas/2010-August/007927.html中展示了使用生成器构建协同程序的一种方法(显然,该代码用于模拟建模)。在这里,有一个&#34;调解员&#34;例如,在每一步之后它返回控制的例程:

def customer(i):
   print("Customer", i, "arriving at", now())
   yield from tables.acquire(1)
   print("Customer", i, "sits down at a table at", now())
   yield from waiters.acquire(1)
   print("Customer", i, "orders spam at", now())
   hold(random.normalvariate(20, 2))
   waiters.release(1)
   print("Customer", i, "gets served spam at", now())
   yield from hold(random.normalvariate(10, 5))
   print("Customer", i, "finished eating at", now())
   tables.release(1)

答案 2 :(得分:0)

你可以这样做,使用iter()重复调用一个函数。将其与队列配对将为您提供阻止行为:

q = Queue()
insert_some_message_periodically_forever(q, period=1)

def get():
    return q.get()

for msg in iter(get, None):
    print('message {0}'.format(msg))

请注意,iter()接受sentinel(例如None),您可以将该值发送到队列以指示流的结束。