天天看點

生成器函數

為了搞清楚<code>yield</code>是用來做什麼的,你首先得知道python中生成器的相關概念,而為了了解生成器的相關概念,你需要知道什麼是疊代器。

疊代器

當你建立一個了清單,你可以逐個周遊清單中的元素,而這個過程便叫做疊代:

而<code>mylist</code>是一個可疊代對象。當你使用清單推導式的時候,建立了一個清單,他也是可疊代對象:

所有能夠接受<code>for...in...</code>操作的對象都是可疊代對象,如清單、字元串、檔案等。這些可疊代對象用起來都十分順手.

因為你可以按照你的想法去通路它們,但是你把所有資料都儲存在了記憶體中,而當你有大量資料的時候這可能并不是你想要的結果。

生成器也是疊代器,但是你隻能對它們進行一次疊代,原因在于它們并沒有将所有資料存儲在記憶體中,而是即時生成這些資料:

這一段代碼和上面那段很相似,唯一不同的地方是使用了<code>()</code>代替<code>[]</code>。但是,這樣的後果是你無法對<code>mygenerator</code>進行第二次<code>for i in mygenerator</code>,因為生成器隻能被使用一次:它首先計算出結果0,然後忘記它再計算出1,最後是4,一個接一個。

<code>yield</code>是一個用法跟<code>return</code>很相似的關鍵字,不同在于函數傳回的是一個生成器。

這是一個沒有什麼用的例子,但是用來讓你了解當你知道你的函數會傳回一個隻會被周遊1次的巨大資料集合該怎麼做的時候十分友善。為了掌握<code>yield</code>,你必須了解當你調用這個函數的時候,你在函數體中寫的代碼并沒有被執行,而是隻傳回了一個生成器對象,這個需要特别注意。然後,你的代碼将會在每次<code>for</code>使用這個生成器的時候被執行。最後,最困難的部分:

<code>for</code>第一次調用通過你函數建立的生成器對象的時候,它将會從你函數的開頭執行代碼,一直到到達<code>yield</code>,然後它将會傳回循環中的第一個值。然後,其他每次調用都會再一次執行你在函數中寫的那段循環,并傳回下一個值,直到沒有值可以傳回。

生成器在函數執行了卻沒有到達<code>yield</code>的時候将被認為是空的,原因在于循環到達了終點,或者不再滿足<code>if/else</code>條件。

首先看生成器的<code>next</code>方法,它用來執行代碼并從生成器中擷取下一個元素(在python 3.x中生成器已經沒有next方法,而是使用next(iterator)代替)。在<code>crisis</code>未被置為<code>true</code>的時候,<code>create_atm</code>函數中的<code>while</code>循環可以看做是無盡的,當<code>crisis</code>為<code>true</code>的時候,跳出了<code>while</code>循環,所有疊代器将會到達函數尾部,此時再次通路<code>next</code>将會抛出<code>stopiteration</code>異常,而此時就算将<code>crisis</code>設定為<code>false</code>,這些生成器仍然處在函數尾部,通路會繼續抛出<code>stopiteration</code>異常。

将以上例子用來控制通路資源等用途的時候十分有用。

<code>itertools</code>子產品包含了許多用來操作可疊代對象的函數。想複制一個生成器?向連接配接兩個生成器?想把多個值組合到一個嵌套清單裡面?使用<code>map/zip</code>而不用重新建立一個清單?那麼就:<code>import itertools</code>吧。

讓我們來看看四匹馬賽跑可能的到達結果:

疊代是一個依賴于可疊代對象(需要實作<code>__iter__()</code>方法)和疊代器(需要實作<code>__next__()</code>方法)的過程。

可疊代對象是任意你可以從中得到一個疊代器的對象。

疊代器是讓你可以對可疊代對象進行疊代的對象。

<code>yield</code>語句将你的函數轉化成一個能夠生成一種能夠包裝你原函數體的名叫生成器的特殊對象的工廠。當生成器被疊代,它将會起始位置開始執行函數一直到到達下一個<code>yield</code>,然後挂起執行,計算傳回傳遞給<code>yield</code>的值,它将會在每次疊代的時候重複這個過程直到函數執行到達函數的尾部,舉例來說:

這種效果的産生是由于在循環中使用了可以産生序列的生成器,生成器在每次循環時執行代碼到下一個<code>yield</code>,并計算傳回結果,這樣生成器即時生成了一個清單,這對于特别是大型計算來說記憶體節省十分有效。

假設你想實作自己的可以産生一個可疊代一定範圍數的<code>range</code>函數(特指python 2.x中的<code>range</code>),你可以這樣做和使用:

但是這樣并不高效,原因1:你建立了一個你隻會使用一次的清單;原因2:這段代碼實際上循環了兩次。

由于guido和他的團隊很慷慨地開發了生成器是以我們可以這樣做:

現在,每次對生成器疊代将會調用<code>next()</code>來執行函數體直到到達<code>yield</code>語句,然後停止執行,并計算傳回結果,或者是到達函數體尾部。在這種情況下,第一次的調用<code>next()</code>将會執行到<code>yield n</code>并傳回<code>n</code>,下一次的<code>next()</code>将會執行自增操作,然後回到<code>while</code>的判斷,如果滿足條件,則再一次停止并傳回<code>n</code>,它将會以這種方式執行一直到不滿足<code>while</code>條件,使得生成器到達函數體尾部。

生成器是這樣一個函數,它記住上一次傳回時在函數體中的位置。對生成器函數的第二次(或第 n 次)調用跳轉至該函數中間,而上次調用的所有局部變量都保持不變。

生成器不僅“記住”了它資料狀态;生成器還“記住”了它在流控制構造(在指令式程式設計中,這種構造不隻是資料值)中的位置。 

生成器的特點:

1.生成器是一個函數,而且函數的參數都會保留。

2.疊代到下一次的調用時,所使用的參數都是第一次所保留下的,即是說,在整個所有函數調用的參數都是第一次所調用時保留的,而不是新建立的

3.節約記憶體

例子:執行到yield時,gen函數暫時停止并儲存,傳回x的值,同時tmp接收send的值(ps:yield x 相當于 return x ,是以第一次c.next()結果是0。第二次c.next()時,繼續在原來暫停的地方執行,因為沒有send 值,是以tmp 為 none。c.next()等價c.send(none))。下次c.send(“python”),send發送過來的值,c.next()等價c.send(none)

了解了next()如何讓包含yield的函數執行後,我們再來看另外一個非常重要的函數send(msg)。其實next()和send()在一定意義上作用是相似的,差別是send()可以傳遞yield表達式的值進去,而next()不能傳遞特定的值,隻能傳遞none進去。是以,我們可以看做c.next()

和 c.send(none) 作用是一樣的。

需要提醒的是,第一次調用時,請使用next()語句或是send(none),不能使用send發送一個非none的值,否則會出錯的,因為沒有python yield語句來接收這個值。

      我們調用一個普通的python函數時,一般是從函數的第一行代碼開始執行,結束于return語句、異常或者函數結束(可以看作隐式的傳回none)。一旦函數将控制權交還給調用者,就意味着全部結束。函數中做的所有工作以及儲存在局部變量中的資料都将丢失。再次調用這個函數時,一切都将從頭建立。 

      對于在計算機程式設計中所讨論的函數,這是很标準的流程。這樣的函數隻能傳回一個值,不過,有時可以建立能産生一個序列的函數還是有幫助的。要做到這一點,這種函數需要能夠“儲存自己的工作”。 

我說過,能夠“産生一個序列”是因為我們的函數并沒有像通常意義那樣傳回。return隐含的意思是函數正将執行代碼的控制權傳回給函數被調用的地方。而"yield"的隐含意思是控制權的轉移是臨時和自願的,我們的函數将來還會收回控制權。

      在python中,擁有這種能力的“函數”被稱為生成器,它非常的有用。生成器(以及yield語句)最初的引入是為了讓程式員可以更簡單的編寫用來産生值的序列的代碼。 以前,要實作類似随機數生成器的東西,需要實作一個類或者一個子產品,在生成資料的同時保持對每次調用之間狀态的跟蹤。引入生成器之後,這變得非常簡單。

為了更好的了解生成器所解決的問題,讓我們來看一個例子。在了解這個例子的過程中,請始終記住我們需要解決的問題:生成值的序列。

      生成器。一個生成器會“生成”值。建立一個生成器幾乎和生成器函數的原理一樣簡單。

一個生成器函數的定義很像一個普通的函數,除了當它要生成一個值的時候,使用yield關鍵字而不是return。如果一個def的主體包含yield,這個函數會自動變成一個生成器(即使它包含一個return)。除了以上内容,建立一個生成器沒有什麼多餘步驟了。

生成器函數傳回生成器的疊代器。這可能是你最後一次見到“生成器的疊代器”這個術語了, 因為它們通常就被稱作“生成器”。要注意的是生成器就是一類特殊的疊代器。作為一個疊代器,生成器必須要定義一些方法(method),其中一個就是__next__()。如同疊代器一樣,我們可以使用next()函數來擷取下一個值。

為了從生成器擷取下一個值,我們使用next()函數,就像對付疊代器一樣。

(next()會操心如何調用生成器的__next__()方法)。既然生成器是一個疊代器,它可以被用在for循環中。

每當生成器被調用的時候,它會傳回一個值給調用者。在生成器内部使用yield來完成這個動作(例如yield 7)。為了記住yield到底幹了什麼,最簡單的方法是把它當作專門給生成器函數用的特殊的return(加上點小魔法)。**

yield就是專門給生成器用的return(加上點小魔法)。

下面是一個簡單的生成器函數:

這裡有兩個簡單的方法來使用它:

那麼神奇的部分在哪裡?我很高興你問了這個問題!當一個生成器函數調用yield,生成器函數的“狀态”會被當機,所有的變量的值會被保留下來,下一行要執行的代碼的位置也會被記錄,直到再次調用next()。一旦next()再次被調用,生成器函數會從它上次離開的地方開始。如果永遠不調用next(),yield儲存的狀态就被無視了。

我們來重寫get_primes()函數,這次我們把它寫作一個生成器。注意我們不再需要magical_infinite_range函數了。使用一個簡單的while循環,我們創造了自己的無窮串列。

如果生成器函數調用了return,或者執行到函數的末尾,會出現一個stopiteration異常。 這會通知next()的調用者這個生成器沒有下一個值了(這就是普通疊代器的行為)。這也是這個while循環在我們的get_primes()函數出現的原因。如果沒有這個while,當我們第二次調用next()的時候,生成器函數會執行到函數末尾,觸發stopiteration異常。一旦生成器的值用完了,再調用next()就會出現錯誤,是以你隻能将每個生成器的使用一次。下面的代碼是錯誤的:

是以,這個while循環是用來確定生成器函數永遠也不會執行到函數末尾的。隻要調用next()這個生成器就會生成一個值。這是一個處理無窮序列的常見方法(這類生成器也是很常見的)。

讓我們回到調用get_primes的地方:solve_number_10。

我們來看一下solve_number_10的for循環中對get_primes的調用,觀察一下前幾個元素是如何建立的有助于我們的了解。當for循環從get_primes請求第一個值時,我們進入get_primes,這時與進入普通函數沒有差別。

進入第三行的while循環

停在if條件判斷(3是素數)

通過yield将3和執行控制權傳回給solve_number_10

接下來,回到insolve_number_10:

for循環得到傳回值3

for循環将其賦給next_prime

total加上next_prime

for循環從get_primes請求下一個值

這次,進入get_primes時并沒有從開頭執行,我們從第5行繼續執行,也就是上次離開的地方。

最關鍵的是,number還保持我們上次調用yield時的值(例如3)。記住,yield會将值傳給next()的調用方,同時還會儲存生成器函數的“狀态”。接下來,number加到4,回到while循環的開始處,然後繼續增加直到得到下一個素數(5)。我們再一次把number的值通過yield傳回給solve_number_10的for循環。這個周期會一直執行,直到for循環結束(得到的素數大于2,000,000)。

我們用前面那個關于素數的函數來展示如何将一個值傳給生成器。這一次,我們不再簡單地生成比某個數大的素數,而是找出比某個數的等比級數大的最小素數(例如10, 我們要生成比10,100,1000,10000 ... 大的最小素數)。我們從get_primes開始:

get_primes的後幾行需要着重解釋。yield關鍵字傳回number的值,而像 other = yield foo 這樣的語句的意思是,"傳回foo的值,這個值傳回給調用者的同時,将other的值也設定為那個值"。你可以通過send方法來将一個值”發送“給生成器。

通過這種方式,我們可以在每次執行yield的時候為number設定不同的值。現在我們可以補齊print_successive_primes中缺少的那部分代碼:

這裡有兩點需要注意:首先,我們列印的是generator.send的結果,這是沒問題的,因為send在發送資料給生成器的同時還傳回生成器通過yield生成的值(就如同生成器中yield語句做的那樣)。

第二點,看一下prime_generator.send(none)這一行,當你用send來“啟動”一個生成器時(就是從生成器函數的第一行代碼執行到第一個yield語句的位置),你必須發送none。這不難了解,根據剛才的描述,生成器還沒有走到第一個yield語句,如果我們發生一個真實的值,這時是沒有人去“接收”它的。一旦生成器啟動了,我們就可以像上面那樣發送資料了。

在本系列文章的後半部分,我們将讨論一些yield的進階用法及其效果。yield已經成為python最強大的關鍵字之一。現在我們已經對yield是如何工作的有了充分的了解,我們已經有了必要的知識,可以去了解yield的一些更“費解”的應用場景。

不管你信不信,我們其實隻是揭開了yield強大能力的一角。例如,send确實如前面說的那樣工作,但是在像我們的例子這樣,隻是生成簡單的序列的場景下,send幾乎從來不會被用到。下面我貼一段代碼,展示send通常的使用方式。對于這段代碼如何工作以及為何可以這樣工作,在此我并不打算多說,它将作為第二部分很不錯的熱身。

 yield指令,可以暫停一個函數并傳回中間結果。使用該指令的函數将儲存執行環境,并且在必要時恢複。

生成器比疊代器更加強大也更加複雜,需要花點功夫好好了解貫通。

看下面一段代碼:

def gen():  

    for x in xrange(4):  

        tmp = yield x  

        if tmp == 'hello':  

            print 'world'  

        else:  

            print str(tmp)  

     隻要函數中包含yield關鍵字,該函數調用就是生成器對象。

g=gen()  

print g   #&lt;generator object gen at 0x02801760&gt;  

print isinstance(g,types.generatortype) #true  

    我們可以看到,gen()并不是函數調用,而是産生生成器對象。

   生成器對象支援幾個方法,如gen.next() ,gen.send() ,gen.throw()等。

print g.next() # 0  

    調用生成器的next方法,将運作到yield位置,此時暫停執行環境,并傳回yield後的值。是以列印出的是0,暫停執行環境。

print g.next() #none  1  

     再調用next方法,你也許會好奇,為啥列印出兩個值,不急,且聽我慢慢道來。

     上一次調用next,執行到yield 0暫停,再次執行恢複環境,給tmp指派(注意:這裡的tmp的值并不是x的值,而是通過send方法接受的值),由于我們沒有調用send方法,是以

tmp的值為none,此時輸出none,并執行到下一次yield x,是以又輸出1.

      到了這裡,next方法我們都懂了,下面看看send方法。

print g.send('hello') #world  2  

      上一次執行到yield 1後暫停,此時我們send('hello'),那麼程式将收到‘hello',并給tmp指派為’hello',此時tmp=='hello'為真,是以輸出'world',并執行到下一次yield 2,是以又列印出2.(next()等價于send(none))

      當循環結束,将抛出stopiteration停止生成器。

      看下面代碼:

def stop_immediately(name):  

    if name == 'skycrab':  

        yield 'okok'  

    else:  

        print 'nono'  

s=stop_immediately('sky')  

s.next()  

正如你所預料的,列印出’nono',由于沒有額外的yield,是以将直接抛出stopiteration。

nono  

traceback (most recent call last):  

  file "f:\python workspace\pytest\src\cs.py", line 170, in &lt;module&gt;  

    s.next()  

stopiteration  

      看下面代碼,了解throw方法,throw主要是向生成器發送異常。

def mygen():  

    try:  

        yield 'something'  

    except valueerror:  

        yield 'value error'  

    finally:  

        print 'clean'  #一定會被執行  

gg=mygen()  

print gg.next() #something  

print gg.throw(valueerror) #value error  clean  

     調用gg.next很明顯此時輸出‘something’,并在yield ‘something’暫停,此時向gg發送valueerror異常,恢複執行環境,except  将會捕捉,并輸出資訊。

     了解了這些,我們就可以向協同程式發起攻擊了,所謂協同程式也就是是可以挂起,恢複,有多個進入點。其實說白了,也就是說多個函數可以同時進行,可以互相之間發送消息等。

     這裡有必要說一下multitask子產品(不是标準庫中的),看一段multitask使用的簡單代碼:

def tt():  

        print 'tt'+str(x)  

        yield  

def gg():  

        print 'xx'+str(x)  

t=multitask.taskmanager()  

t.add(tt())  

t.add(gg())  

t.run()  

結果:

tt0  

xx0  

tt1  

xx1  

tt2  

xx2  

tt3  

xx3  

   如果不是使用生成器,那麼要實作上面現象,即函數交錯輸出,那麼隻能使用線程了,是以生成器給我們提供了更廣闊的前景。 

   如果僅僅是實作上面的效果,其實很簡單,我們可以自己寫一個。主要思路就是将生成器對象放入隊列,執行send(none)後,如果沒有抛出stopiteration,将該生成器對象再加入隊列。

class task():  

    def __init__(self):  

        self._queue = queue.queue()  

    def add(self,gen):  

        self._queue.put(gen)  

    def run(self):  

        while not self._queue.empty():  

            for i in xrange(self._queue.qsize()):  

                try:  

                    gen= self._queue.get()  

                    gen.send(none)  

                except stopiteration:  

                    pass  

                else:  

                    self._queue.put(gen)  

t=task()  

  當然,multitask實作的肯定不止這個功能,有興趣的童鞋可以看下源碼,還是比較簡單易懂的。

#增補 2014/5/21

之前我在南京面試python時遇到這麼一道題目:

def thread1():  

    for x in range(4):  

        yield  x  

def thread2():  

    for x in range(4,8):  

threads=[]  

threads.append(thread1())  

threads.append(thread2())  

def run(threads): #寫這個函數,模拟線程并發  

    pass  

run(threads)  

如果上面class task看懂了,那麼這題很簡單,其實就是考你用yield模拟線程排程,解決如下:

def run(threads):  

    for t in threads:  

        try:  

            print t.next()  

        except stopiteration:  

            pass  

            threads.append(t)