簡體   English   中英

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

[英]What is a strategy for creating reactive iterators in Python?

我一直在閱讀Javascript世界中的功能性反應式編程方面的許多令人振奮的進展。 我也被Python的迭代器協議迷住了。 我知道迭代器可用於構建協同例程,現在我在想,構建反應式迭代器的方法是什么,我們稱其為“流”,這樣在流上進行迭代將阻塞直到一個新值傳遞到流中?

這是我想做的一個例子:

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還是coroutine或其他。

我認為我想做的是避免使用信號/回調模式,因為回調在Python中是如此笨拙,除非它們可以容納在lambda 這就是我所說的“反應式迭代器”的含義-流程控制被反轉,並且for循環的主體(或在流上進行迭代的任何內容)變為反應性而不是主動性,本質上是一個內嵌的塊回調,每當項目輸入流。

那么,這以前做過嗎? 如果不是,什么模式/庫/什么可以使它起作用? GEVENT? 龍卷風的IOLoop? greenlets?

這看起來很像用迭代器協議包裝線程和排隊。

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的評論

您可以使用greenlets,lessstack或任何其他輕量級並發編程庫/系統來重新實現我的示例,示例的基本原理將保持不變。 流(以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()

問題在於,通過for循環完成生成器上的next調用,本質上與調用以None為參數的send相同。 由於供應商和使用者for循環之間沒有同步,因此使用者生成器有可能接收以下輸入序列

  • 無(開始運行;在位置A收到)
  • 1(來自供應商;在位置B收到)
  • 無(從下一個for循環調用開始;在位置C接收)
  • 無(由於供應商仍在睡覺,因此從下一次的for循環調用開始;在位置B收到)
  • 無(由於供應商仍在睡覺,因此從下一個for循環調用開始;在位置C收到)

這意味着生成器將屈服於for循環:[1,無,無,無,...]。 根據供應商再次踢進去,發送到位置B或C的時間,for循環可能甚至看不到2、3等。因此,事實證明,如果您要使用協程作為迭代器,則顯然使用隊列(或其他同步方法)來避免此問題。 如果有方法指定要屈服的位置 ,例如,如果從供應商處調用,則僅在此處屈服,而不是for循環,否則阻塞。

您一定在談論發電機 從生成器讀取代碼(=調用其next() )會阻塞,直到生成器yield某種效果為止。 隨着時間的推移,進行了一些增強( PEP 342-2.5PEP 380-3.3 ),以簡化將生成器用作協程的使用。

PEP 380的冠軍Greg Ewing在https://mail.python.org/pipermail/python-ideas/2010-August/007927.html中展示了一種使用生成器構建協程的方法(顯然,該代碼用於仿真造型)。 在此程序上方有一個“調解員”例程,它在每一步后將控制權返回給該例程:

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)

您可以通過使用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()接受一個哨兵(例如None),您也可以將該值發送到隊列以指示流結束。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM