摘要:消息向迭代器獲取所表示的底層序列的下一個元素。為了對方法調(diào)用做出回應(yīng),迭代器可以執(zhí)行任何計算來獲取或計算底層數(shù)據(jù)序列的下一個元素。這個迭代器應(yīng)擁有方法,依次返回序列中的每個元素,最后到達(dá)序列末尾時產(chǎn)生異常。
第五章 序列和協(xié)程
5.1 引言來源:Chapter 5: Sequences and Coroutines
譯者:飛龍
協(xié)議:CC BY-NC-SA 4.0
在這一章中,我們通過開發(fā)新的工具來處理有序數(shù)據(jù),繼續(xù)討論真實(shí)世界中的應(yīng)用。在第二張中,我們介紹了序列接口,在 Python 內(nèi)置的數(shù)據(jù)類型例如tuple和list中實(shí)現(xiàn)。序列支持兩個操作:獲取長度和由下標(biāo)訪問元素。第三章中,我們開發(fā)了序列接口的用戶定義實(shí)現(xiàn),用于表示遞歸列表的Rlist類。序列類型具有高效的表現(xiàn)力,并且可以讓我們高效訪問大量有序數(shù)據(jù)集。
但是,使用序列抽象表示有序數(shù)據(jù)有兩個重要限制。第一個是長度為n的序列的要占據(jù)比例為n的內(nèi)存總數(shù)。于是,序列越長,表示它所占的內(nèi)存空間就越大。
第二個限制是,序列只能表示已知且長度有限的數(shù)據(jù)集。許多我們想要表示的有序集合并沒有定義好的長度,甚至有些是無限的。兩個無限序列的數(shù)學(xué)示例是正整數(shù)和斐波那契數(shù)。無限長度的有序數(shù)據(jù)集也出現(xiàn)在其它計算領(lǐng)域,例如,所有推特狀態(tài)的序列每秒都在增長,所以并沒有固定的長度。與之類似,經(jīng)過基站發(fā)送出的電話呼叫序列,由計算機(jī)用戶發(fā)出的鼠標(biāo)動作序列,以及飛機(jī)上的傳感器產(chǎn)生的加速度測量值序列,都在世界演化過程中無限擴(kuò)展。
在這一章中,我們介紹了新的構(gòu)造方式用于處理有序數(shù)據(jù),它為容納未知或無限長度的集合而設(shè)計,但僅僅使用有限的內(nèi)存。我們也會討論這些工具如何用于一種叫做協(xié)程的程序結(jié)構(gòu),來創(chuàng)建高效、模塊化的數(shù)據(jù)處理流水線。
5.2 隱式序列序列可以使用一種程序結(jié)構(gòu)來表示,它不將每個元素顯式儲存在內(nèi)存中,這是高效處理有序數(shù)據(jù)的核心概念。為了將這個概念用于實(shí)踐,我們需要構(gòu)造對象來提供序列中所有元素的訪問,但是不要事先把所有元素計算出來并儲存。
這個概念的一個簡單示例就是第二章出現(xiàn)的range序列類型。range表示連續(xù)有界的整數(shù)序列。但是,它的每個元素并不顯式在內(nèi)存中表示,當(dāng)元素從range中獲取時,才被計算出來。所以,我們可以表示非常大的整數(shù)范圍。只有范圍的結(jié)束位置才被儲存為range對象的一部分,元素都被憑空計算出來。
>>> r = range(10000, 1000000000) >>> r[45006230] 45016230
這個例子中,當(dāng)構(gòu)造范圍示例時,并不是這個范圍內(nèi)的所有 999,990,000 個整數(shù)都被儲存。反之,范圍對象將第一個元素 10,000 與下標(biāo)相加 45,006,230 來產(chǎn)生第 45,016,230 個元素。計算所求的元素值并不從現(xiàn)有的表示中獲取,這是惰性計算的一個例子。計算機(jī)科學(xué)將惰性作為一種重要的計算工具加以贊揚(yáng)。
迭代器是提供底層有序數(shù)據(jù)集的有序訪問的對象。迭代器在許多編程語言中都是內(nèi)建對象,包括 Python。迭代器抽象擁有兩個組成部分:一種獲取底層元素序列的下一個元素的機(jī)制,以及一種標(biāo)識元素序列已經(jīng)到達(dá)末尾,沒有更多剩余元素的機(jī)制。在帶有內(nèi)建對象系統(tǒng)的編程語言中,這個抽象通常相當(dāng)于可以由類實(shí)現(xiàn)的特定接口。Python 的迭代器接口會在下一節(jié)中描述。
迭代器的實(shí)用性來源于一個事實(shí),底層數(shù)據(jù)序列并不能顯式在內(nèi)存中表達(dá)。迭代器提供了一種機(jī)制,可以依次計算序列中的每個值,但是所有元素不需要連續(xù)儲存。反之,當(dāng)下個元素從迭代器獲取的時候,這個元素會按照請求計算,而不是從現(xiàn)有的內(nèi)存來源中獲取。
范圍可以惰性計算序列中的元素,因?yàn)樾蛄械谋硎臼墙y(tǒng)一的,并且任何元素都可以輕易從范圍的起始和結(jié)束位置計算出來。迭代器支持更廣泛的底層有序數(shù)據(jù)集的惰性生成,因?yàn)樗鼈儾恍枰峁┑讓有蛄腥我庠氐脑L問途徑。反之,它們僅僅需要按照順序,在每次其它元素被請求的時候,計算出序列的下一個元素。雖然不像序列可訪問任意元素那樣靈活(叫做隨機(jī)訪問),有序數(shù)據(jù)序列的順序訪問對于數(shù)據(jù)處理應(yīng)用來說已經(jīng)足夠了。
5.2.1 Python 迭代器Python 迭代器接口包含兩個消息。__next__消息向迭代器獲取所表示的底層序列的下一個元素。為了對__next__方法調(diào)用做出回應(yīng),迭代器可以執(zhí)行任何計算來獲取或計算底層數(shù)據(jù)序列的下一個元素。__next__的調(diào)用讓迭代器產(chǎn)生變化:它們向前移動迭代器的位置。所以多次調(diào)用__next__會有序返回底層序列的元素。在__next__的調(diào)用過程中,Python 通過StopIteration異常,來表示底層數(shù)據(jù)序列已經(jīng)到達(dá)末尾。
下面的Letters類迭代了從a到d字母的底層序列。成員變量current儲存了序列中的當(dāng)前字母。__next__方法返回這個字母,并且使用它來計算current的新值。
>>> class Letters(object): def __init__(self): self.current = "a" def __next__(self): if self.current > "d": raise StopIteration result = self.current self.current = chr(ord(result)+1) return result def __iter__(self): return self
__iter__消息是 Python 迭代器所需的第二個消息。它只是簡單返回迭代器,它對于提供迭代器和序列的通用接口很有用,在下一節(jié)會描述。
使用這個類,我們就可以訪問序列中的字母:
>>> letters = Letters() >>> letters.__next__() "a" >>> letters.__next__() "b" >>> letters.__next__() "c" >>> letters.__next__() "d" >>> letters.__next__() Traceback (most recent call last): File "", line 1, in File " ", line 12, in next StopIteration
Letters示例只能迭代一次。一旦__next__()方法產(chǎn)生了StopIteration異常,它就從此之后一直這樣了。除非創(chuàng)建新的實(shí)例,否則沒有辦法來重置它。
迭代器也允許我們表示無限序列,通過實(shí)現(xiàn)永遠(yuǎn)不會產(chǎn)生StopIteration異常的__next__方法。例如,下面展示的Positives類迭代了正整數(shù)的無限序列:
>>> class Positives(object): def __init__(self): self.current = 0; def __next__(self): result = self.current self.current += 1 return result def __iter__(self): return self5.2.2 for語句
Python 中,序列可以通過實(shí)現(xiàn)__iter__消息用于迭代。如果一個對象表示有序數(shù)據(jù),它可以在for語句中用作可迭代對象,通過回應(yīng)__iter__消息來返回迭代器。這個迭代器應(yīng)擁有__next__()方法,依次返回序列中的每個元素,最后到達(dá)序列末尾時產(chǎn)生StopIteration異常。
>>> counts = [1, 2, 3] >>> for item in counts: print(item) 1 2 3
在上面的實(shí)例中,counts列表返回了迭代器,作為__iter__()方法調(diào)用的回應(yīng)。for語句之后反復(fù)調(diào)用迭代器的__next__()方法,并且每次都將返回值賦給item。這個過程一直持續(xù),直到迭代器產(chǎn)生了StopIteration異常,這時for語句就終止了。
使用我們關(guān)于迭代器的知識,我們可以拿while、賦值和try語句實(shí)現(xiàn)for語句的求值規(guī)則:
>>> i = counts.__iter__() >>> try: while True: item = i.__next__() print(item) except StopIteration: pass 1 2 3
在上面,調(diào)用counts的__iter__方法所返回的迭代器綁定到了名稱i上面,便于依次獲取每個元素。StopIteration異常的處理子句不做任何事情,但是這個異常的處理提供了退出while循環(huán)的控制機(jī)制。
5.2.3 生成器和yield語句上面的Letters和Positives對象需要我們引入一種新的字段,self.current,來跟蹤序列的處理過程。在上面所示的簡單序列中,這可以輕易實(shí)現(xiàn)。但對于復(fù)雜序列,__next__()很難在計算中節(jié)省空間。生成器允許我們通過利用 Python 解釋器的特性定義更復(fù)雜的迭代。
生成器是由一類特殊函數(shù),叫做生成器函數(shù)返回的迭代器。生成器函數(shù)不同于普通的函數(shù),因?yàn)樗辉诤瘮?shù)體中包含return語句,而是使用yield語句來返回序列中的元素。
生成器不使用任何對象屬性來跟蹤序列的處理過程。它們控制生成器函數(shù)的執(zhí)行,每次__next__方法調(diào)用時,它們執(zhí)行到下一個yield語句。Letters迭代可以使用生成器函數(shù)實(shí)現(xiàn)得更加簡潔。
>>> def letters_generator(): current = "a" while current <= "d": yield current current = chr(ord(current)+1) >>> for letter in letters_generator(): print(letter) a b c d
即使我們永不顯式定義__iter__()或__next__()方法,Python 會理解當(dāng)我們使用yield語句時,我們打算定義生成器函數(shù)。調(diào)用時,生成器函數(shù)并不返回特定的產(chǎn)出值,而是返回一個生成器(一種迭代器),它自己就可以返回產(chǎn)出的值。生成器對象擁有__iter__和__next__方法,每個對__next__的調(diào)用都會從上次停留的地方繼續(xù)執(zhí)行生成器函數(shù),直到另一個yield語句執(zhí)行的地方。
__next__第一次調(diào)用時,程序從letters_generator的函數(shù)體一直執(zhí)行到進(jìn)入yield語句。之后,它暫停并返回current值。yield語句并不破壞新創(chuàng)建的環(huán)境,而是為之后的使用保留了它。當(dāng)__next__再次調(diào)用時,執(zhí)行在它停留的地方恢復(fù)。letters_generator作用域中current和任何所綁定名稱的值都會在隨后的__next__調(diào)用中保留。
我們可以通過手動調(diào)用__next__()來遍歷生成器:
>>> letters = letters_generator() >>> type(letters)>>> letters.__next__() "a" >>> letters.__next__() "b" >>> letters.__next__() "c" >>> letters.__next__() "d" >>> letters.__next__() Traceback (most recent call last): File " ", line 1, in StopIteration
在第一次__next__()調(diào)用之前,生成器并不會開始執(zhí)行任何生成器函數(shù)體中的語句。
5.2.4 可迭代對象Python 中,迭代只會遍歷一次底層序列的元素。在遍歷之后,迭代器在__next__()調(diào)用時會產(chǎn)生StopIteration異常。許多應(yīng)用需要迭代多次元素。例如,我們需要對一個列表迭代多次來枚舉所有的元素偶對:
>>> def all_pairs(s): for item1 in s: for item2 in s: yield (item1, item2) >>> list(all_pairs([1, 2, 3])) [(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3)]
序列本身不是迭代器,但是它是可迭代對象。Python 的可迭代接口只包含一個消息,__iter__,返回一個迭代器。Python 中內(nèi)建的序列類型在__iter__方法調(diào)用時,返回迭代器的新實(shí)例。如果一個可迭代對象在每次調(diào)用__iter__時返回了迭代器的新實(shí)例,那么它就能被迭代多次。
新的可迭代類可以通過實(shí)現(xiàn)可迭代接口來定義。例如,下面的可迭代對象LetterIterable類在每次調(diào)用__iter__時返回新的迭代器來迭代字母。
>>> class LetterIterable(object): def __iter__(self): current = "a" while current <= "d": yield current current = chr(ord(current)+1)
__iter__方法是個生成器函數(shù),它返回一個生成器對象,產(chǎn)出從"a"到"d"的字母。
Letters迭代器對象在單次迭代之后就被“用完”了,但是LetterIterable對象可被迭代多次。所以,LetterIterable示例可以用于all_pairs的參數(shù)。
>>> letters = LetterIterable() >>> all_pairs(letters).__next__() ("a", "a")5.2.5 流
流提供了一種隱式表示有序數(shù)據(jù)的最終方式。流是惰性計算的遞歸列表。就像第三章的Rlist類那樣,Stream實(shí)例可以響應(yīng)對其第一個元素和剩余部分的獲取請求。同樣,Stream的剩余部分還是Stream。然而不像RList,流的剩余部分只在查找時被計算,而不是事先存儲。也就是說流的剩余部分是惰性計算的。
為了完成這個惰性求值,流會儲存計算剩余部分的函數(shù)。無論這個函數(shù)在什么時候調(diào)用,它的返回值都作為流的一部分,儲存在叫做_rest的屬性中。下劃線表示它不應(yīng)直接訪問??稍L問的屬性rest是個方法,它返回流的剩余部分,并在必要時計算它。使用這個設(shè)計,流可以儲存計算剩余部分的方式,而不用總是顯式儲存它們。
>>> class Stream(object): """A lazily computed recursive list.""" def __init__(self, first, compute_rest, empty=False): self.first = first self._compute_rest = compute_rest self.empty = empty self._rest = None self._computed = False @property def rest(self): """Return the rest of the stream, computing it if necessary.""" assert not self.empty, "Empty streams have no rest." if not self._computed: self._rest = self._compute_rest() self._computed = True return self._rest def __repr__(self): if self.empty: return "" return "Stream({0}, )".format(repr(self.first)) >>> Stream.empty = Stream(None, None, True)
遞歸列表可使用嵌套表達(dá)式來定義。例如,我們可以創(chuàng)建RList,來表達(dá)1和5的序列,像下面這樣:
>>> r = Rlist(1, Rlist(2+3, Rlist.empty))
與之類似,我們可以創(chuàng)建一個Stream來表示相同序列。Stream在請求剩余部分之前,并不會實(shí)際計算下一個元素5。
>>> s = Stream(1, lambda: Stream(2+3, lambda: Stream.empty))
這里,1是流的第一個元素,后面的lambda表達(dá)式是用于計算流的剩余部分的函數(shù)。被計算的流的第二個元素又是一個返回空流的函數(shù)。
訪問遞歸列表r和流s中的元素?fù)碛邢嗨频倪^程。但是,5儲存在了r之中,而對于s來說,它在首次被請求時通過加法來按要求計算。
>>> r.first 1 >>> s.first 1 >>> r.rest.first 5 >>> s.rest.first 5 >>> r.rest Rlist(5) >>> s.rest Stream(5,)
當(dāng)make_integer_stream首次被調(diào)用時,它返回了一個流,流的first是序列中第一個整數(shù)(默認(rèn)為1)。但是,make_integer_stream實(shí)際是遞歸的,因?yàn)檫@個流的compute_rest以自增的參數(shù)再次調(diào)用了make_integer_stream。這會讓make_integer_stream變成遞歸的,同時也是惰性的。
>>> ints.first 1 >>> ints.rest.first 2 >>> ints.rest.rest Stream(3,)
無論何時請求整數(shù)流的rest,都僅僅遞歸調(diào)用make_integer_stream。
操作序列的相同高階函數(shù) -- map和filter -- 同樣可應(yīng)用于流,雖然它們的實(shí)現(xiàn)必須修改來惰性調(diào)用它們的參數(shù)函數(shù)。map_stream在一個流上映射函數(shù),這會產(chǎn)生一個新的流。局部定義的compute_rest函數(shù)確保了無論什么時候rest被計算出來,這個函數(shù)都會在流的剩余部分上映射。
>>> def map_stream(fn, s): if s.empty: return s def compute_rest(): return map_stream(fn, s.rest) return Stream(fn(s.first), compute_rest)
流可以通過定義compute_rest函數(shù)來過濾,這個函數(shù)在流的剩余部分上調(diào)用過濾器函數(shù)。如果過濾器函數(shù)拒絕了流的第一個元素,剩余部分會立即計算出來。因?yàn)?b>filter_stream是遞歸的,剩余部分可能會多次計算直到找到了有效的first元素。
>>> def filter_stream(fn, s): if s.empty: return s def compute_rest(): return filter_stream(fn, s.rest) if fn(s.first): return Stream(s.first, compute_rest) return compute_rest()
map_stream和filter_stream展示了流式處理的常見模式:無論流的剩余部分何時被計算,局部定義的compute_rest函數(shù)都會對流的剩余部分遞歸調(diào)用某個處理函數(shù)。
為了觀察流的內(nèi)容,我們需要將其截斷為有限長度,并轉(zhuǎn)換為 Python list。
>>> def truncate_stream(s, k): if s.empty or k == 0: return Stream.empty def compute_rest(): return truncate_stream(s.rest, k-1) return Stream(s.first, compute_rest) >>> def stream_to_list(s): r = [] while not s.empty: r.append(s.first) s = s.rest return r
這些便利的函數(shù)允許我們驗(yàn)證map_stream的實(shí)現(xiàn),使用一個非常簡單的例子,從3到7的整數(shù)平方。
>>> s = make_integer_stream(3) >>> s Stream(3,) >>> m = map_stream(lambda x: x*x, s) >>> m Stream(9, ) >>> stream_to_list(truncate_stream(m, 5)) [9, 16, 25, 36, 49]
我們可以使用我們的filter_stream函數(shù)來定義素數(shù)流,使用埃拉托斯特尼篩法(sieve of Eratosthenes),它對整數(shù)流進(jìn)行過濾,移除第一個元素的所有倍數(shù)數(shù)值。通過成功過濾出每個素數(shù),所有合數(shù)都從流中移除了。
>>> def primes(pos_stream): def not_divible(x): return x % pos_stream.first != 0 def compute_rest(): return primes(filter_stream(not_divible, pos_stream.rest)) return Stream(pos_stream.first, compute_rest)
通過截斷primes流,我們可以枚舉素數(shù)的任意前綴:
>>> p1 = primes(make_integer_stream(2)) >>> stream_to_list(truncate_stream(p1, 7)) [2, 3, 5, 7, 11, 13, 17]
流和迭代器不同,因?yàn)樗鼈兛梢远啻蝹鬟f給純函數(shù),并且每次都產(chǎn)生相同的值。素數(shù)流并沒有在轉(zhuǎn)換為列表之后“用完”。也就是說,在將流的前綴轉(zhuǎn)換為列表之后,p1的第一個元素仍舊是2。
>>> p1.first 2
就像遞歸列表提供了序列抽象的簡單實(shí)現(xiàn),流提供了簡單、函數(shù)式的遞歸數(shù)據(jù)結(jié)構(gòu),它通過高階函數(shù)的使用實(shí)現(xiàn)了惰性求值。
5.3 協(xié)程這篇文章的大部分專注于將復(fù)雜程序解構(gòu)為小型、模塊化組件的技巧。當(dāng)一個帶有復(fù)雜行為的函數(shù)邏輯劃分為幾個獨(dú)立的、本身為函數(shù)的步驟時,這些函數(shù)叫做輔助函數(shù)或者子過程。子過程由主函數(shù)調(diào)用,主函數(shù)負(fù)責(zé)協(xié)調(diào)子函數(shù)的使用。
這一節(jié)中,我們使用協(xié)程,引入了一種不同的方式來解構(gòu)復(fù)雜的計算。它是一種針對有序數(shù)據(jù)的任務(wù)處理方式。就像子過程那樣,協(xié)程會計算復(fù)雜計算的一小步。但是,在使用協(xié)程時,沒有主函數(shù)來協(xié)調(diào)結(jié)果。反之,協(xié)程會自發(fā)鏈接到一起來組成流水線。可能有一些協(xié)程消耗輸入數(shù)據(jù),并把它發(fā)送到其它協(xié)程。也可能有一些協(xié)程,每個都對發(fā)送給它的數(shù)據(jù)執(zhí)行簡單的處理步驟。最后可能有另外一些協(xié)程輸出最終結(jié)果。
協(xié)程和子過程的差異是概念上的:子過程在主函數(shù)中位于下級,但是協(xié)程都是平等的,它們協(xié)作組成流水線,不帶有任何上級函數(shù)來負(fù)責(zé)以特定順序調(diào)用它們。
這一節(jié)中,我們會學(xué)到 Python 如何通過yield和send()語句來支持協(xié)程的構(gòu)建。之后,我們會看到協(xié)程在流水線中的不同作用,以及協(xié)程如何支持多任務(wù)。
5.3.1 Python 協(xié)程在之前一節(jié)中,我們介紹了生成器函數(shù),它使用yield來返回一個值。Python 的生成器函數(shù)也可以使用(yield)語句來接受一個值。生成器對象上有兩個額外的方法:send()和close(),創(chuàng)建了一個模型使對象可以消耗或產(chǎn)出值。定義了這些對象的生成器函數(shù)叫做協(xié)程。
協(xié)程可以通過(yield)語句來消耗值,向像下面這樣:
value = (yield)
使用這個語法,在帶參數(shù)調(diào)用對象的send方法之前,執(zhí)行流會停留在這條語句上。
coroutine.send(data)
之后,執(zhí)行會恢復(fù),value會被賦為data的值。為了發(fā)射計算終止的信號,我們需要使用close()方法來關(guān)閉協(xié)程。這會在協(xié)程內(nèi)部產(chǎn)生GeneratorExit異常,它可以由try/except子句來捕獲。
下面的例子展示了這些概念。它是一個協(xié)程,用于打印匹配所提供的模式串的字符串。
>>> def match(pattern): print("Looking for " + pattern) try: while True: s = (yield) if pattern in s: print(s) except GeneratorExit: print("=== Done ===")
我們可以使用一個模式串來初始化它,之后調(diào)用__next__()來開始執(zhí)行:
>>> m = match("Jabberwock") >>> m.__next__() Looking for Jabberwock
對__next__()的調(diào)用會執(zhí)行函數(shù)體,所以"Looking for jabberwock"會被打印。語句會一直持續(xù)執(zhí)行,直到遇到line = (yield)語句。之后,執(zhí)行會暫停,并且等待一個發(fā)送給m的值。我們可以使用send來將值發(fā)送給它。
>>> m.send("the Jabberwock with eyes of flame") the Jabberwock with eyes of flame >>> m.send("came whiffling through the tulgey wood") >>> m.send("and burbled as it came") >>> m.close() === Done ===
當(dāng)我們以一個值調(diào)用m.send時,協(xié)程m內(nèi)部的求值會在line = (yield)語句處恢復(fù),這里會把發(fā)送的值賦給line變量。m中的語句會繼續(xù)求值,如果匹配的話會打印出那一行,并繼續(xù)執(zhí)行循環(huán),直到再次進(jìn)入line = (yield)。之后,m中的求值會暫停,并在m.send調(diào)用后恢復(fù)。
我們可以將使用send()和yield的函數(shù)鏈到一起來完成復(fù)雜的行為。例如,下面的函數(shù)將名為text的字符串分割為單詞,并把每個單詞發(fā)送給另一個協(xié)程。
每個單詞都發(fā)送給了綁定到next_coroutine的協(xié)程,使next_coroutine開始執(zhí)行,而且這個函數(shù)暫停并等待。它在next_coroutine暫停之前會一直等待,隨后這個函數(shù)恢復(fù)執(zhí)行,發(fā)送下一個單詞或執(zhí)行完畢。
如果我們將上面定義的match和這個函數(shù)鏈到一起,我們就可以創(chuàng)建出一個程序,只打印出匹配特定單詞的單詞。
>>> text = "Commending spending is offending to people pending lending!" >>> matcher = match("ending") >>> matcher.__next__() Looking for ending >>> read(text, matcher) Commending spending offending pending lending! === Done ===
read函數(shù)向協(xié)程matcher發(fā)送每個單詞,協(xié)程打印出任何匹配pattern的輸入。在matcher協(xié)程中,s = (yield)一行等待每個發(fā)送進(jìn)來的單詞,并且在執(zhí)行到這一行之后將控制流交還給read。
5.3.2 生產(chǎn)、過濾和消耗協(xié)程基于如何使用yield和send()而具有不同的作用:
生產(chǎn)者創(chuàng)建序列中的物品,并使用send(),而不是(yield)。
過濾器使用(yield)來消耗物品并將結(jié)果使用send()發(fā)送給下一個步驟。
消費(fèi)者使用(yield)來消耗物品,但是從不發(fā)送。
上面的read函數(shù)是一個生產(chǎn)者的例子。它不使用(yield),但是使用send來生產(chǎn)數(shù)據(jù)。函數(shù)match是個消費(fèi)者的例子。它不使用send發(fā)送任何東西,但是使用(yield)來消耗數(shù)據(jù)。我們可以將match拆分為過濾器和消費(fèi)者。過濾器是一個協(xié)程,只發(fā)送與它的模式相匹配的字符串。
>>> def match_filter(pattern, next_coroutine): print("Looking for " + pattern) try: while True: s = (yield) if pattern in s: next_coroutine.send(s) except GeneratorExit: next_coroutine.close()
消費(fèi)者是一個函數(shù),只打印出發(fā)送給它的行:
>>> def print_consumer(): print("Preparing to print") try: while True: line = (yield) print(line) except GeneratorExit: print("=== Done ===")
當(dāng)過濾器或消費(fèi)者被構(gòu)建時,必須調(diào)用它的__next__方法來開始執(zhí)行:
>>> printer = print_consumer() >>> printer.__next__() Preparing to print >>> matcher = match_filter("pend", printer) >>> matcher.__next__() Looking for pend >>> read(text, matcher) spending pending === Done ===
即使名稱filter暗示移除元素,過濾器也可以轉(zhuǎn)換元素。下面的函數(shù)是個轉(zhuǎn)換元素的過濾器的示例。它消耗字符串并發(fā)送一個字典,包含了每個不同的字母在字符串中的出現(xiàn)次數(shù)。
>>> def count_letters(next_coroutine): try: while True: s = (yield) counts = {letter:s.count(letter) for letter in set(s)} next_coroutine.send(counts) except GeneratorExit as e: next_coroutine.close()
我們可以使用它來計算文本中最常出現(xiàn)的字母,并使用一個消費(fèi)者,將字典合并來找出最常出現(xiàn)的鍵。
>>> def sum_dictionaries(): total = {} try: while True: counts = (yield) for letter, count in counts.items(): total[letter] = count + total.get(letter, 0) except GeneratorExit: max_letter = max(total.items(), key=lambda t: t[1])[0] print("Most frequent letter: " + max_letter)
為了在文件上運(yùn)行這個流水線,我們必須首先按行讀取文件。之后,將結(jié)果發(fā)送給count_letters,最后發(fā)送給sum_dictionaries。我們可以服用read協(xié)程來讀取文件中的行。
>>> s = sum_dictionaries() >>> s.__next__() >>> c = count_letters(s) >>> c.__next__() >>> read(text, c) Most frequent letter: n5.3.3 多任務(wù)
生產(chǎn)者或過濾器并不受限于唯一的下游。它可以擁有多個協(xié)程作為它的下游,并使用send()向它們發(fā)送數(shù)據(jù)。例如,下面是read的一個版本,向多個下游發(fā)送字符串中的單詞:
>>> def read_to_many(text, coroutines): for word in text.split(): for coroutine in coroutines: coroutine.send(word) for coroutine in coroutines: coroutine.close()
我們可以使用它來檢測多個單詞中的相同文本:
>>> m = match("mend") >>> m.__next__() Looking for mend >>> p = match("pe") >>> p.__next__() Looking for pe >>> read_to_many(text, [m, p]) Commending spending people pending === Done === === Done ===
首先,read_to_many在m上調(diào)用了send(word)。這個協(xié)程正在等待循環(huán)中的text = (yield),之后打印出所發(fā)現(xiàn)的匹配,并且等待下一個send。之后執(zhí)行流返回到了read_to_many,它向p發(fā)送相同的行。所以,text中的單詞會按照順序打印出來。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/38109.html
摘要:清理程序通過對記錄已知計數(shù)器的有序集合執(zhí)行命令來一個接一個的遍歷所有已知的計數(shù)器。 上一篇文章:Python--Redis實(shí)戰(zhàn):第五章:使用Redis構(gòu)建支持程序:第1節(jié):使用Redis來記錄日志下一篇文章:Python--Redis實(shí)戰(zhàn):第五章:使用Redis構(gòu)建支持程序:第3節(jié):查找IP所屬城市以及國家 正如第三章所述,通過記錄各個頁面的被訪問次數(shù),我們可以根據(jù)基本的訪問計數(shù)信息...
摘要:進(jìn)程線程和協(xié)程進(jìn)程的定義進(jìn)程,是計算機(jī)中已運(yùn)行程序的實(shí)體。協(xié)程和線程的關(guān)系協(xié)程是在語言層面實(shí)現(xiàn)對線程的調(diào)度,避免了內(nèi)核級別的上下文消耗。和都引入了消息調(diào)度系統(tǒng)模型,來避免鎖的影響和進(jìn)程線程開銷大的問題。 進(jìn)程、線程和協(xié)程 進(jìn)程的定義: 進(jìn)程,是計算機(jī)中已運(yùn)行程序的實(shí)體。程序本身只是指令、數(shù)據(jù)及其組織形式的描述,進(jìn)程才是程序的真正運(yùn)行實(shí)例。 線程的定義: 操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單...
摘要:并發(fā)用于制定方案,用來解決可能但未必并行的問題。在協(xié)程中使用需要注意兩點(diǎn)使用鏈接的多個協(xié)程最終必須由不是協(xié)程的調(diào)用方驅(qū)動,調(diào)用方顯式或隱式在最外層委派生成器上調(diào)用函數(shù)或方法。對象可以取消取消后會在協(xié)程當(dāng)前暫停的處拋出異常。 導(dǎo)語:本文章記錄了本人在學(xué)習(xí)Python基礎(chǔ)之控制流程篇的重點(diǎn)知識及個人心得,打算入門Python的朋友們可以來一起學(xué)習(xí)并交流。 本文重點(diǎn): 1、了解asyncio...
摘要:所解包的序列中的元素數(shù)量必須和賦值符號左邊的變量數(shù)量完全一致。其中,冒號標(biāo)識語句塊開始塊中每一個語句都是縮進(jìn)相同量退回到和已經(jīng)閉合的塊一樣的縮進(jìn)量時,表示當(dāng)前塊結(jié)束。成員資格運(yùn)算符字符串和序列比較字符串可按照字母順序比較。 print和import print打印多個表達(dá)式,用逗號,隔開 print abc:, 42, nonono #輸出在每個參數(shù)之間添加空格 print在結(jié)尾處加上...
閱讀 2952·2023-04-25 19:20
閱讀 814·2021-11-24 09:38
閱讀 2066·2021-09-26 09:55
閱讀 2443·2021-09-02 15:11
閱讀 2075·2019-08-30 15:55
閱讀 3621·2019-08-30 15:54
閱讀 3159·2019-08-30 14:03
閱讀 2972·2019-08-29 17:11