【Python】迭代器、生成器、yield單執行緒非同步併發實現詳解

ai3707發表於2016-04-12
     大家在學習python開發時可能經常對迭代器、生成器、yield關鍵字用法有所疑惑,在這篇文章將從理論+程式除錯驗證的方式詳細講解這部分知識,話不多說,直接進入主題

一、迭代器(Iterater):
     首先介紹迭代器,迭代器是訪問集合元素的一種方式,迭代器物件從集合的第一個元素開始訪問,直到所有的元素被訪問完結束。是不是覺得跟for迴圈很像?但是迭代器有幾個特性需記住:
    1、訪問者不需要關心迭代器內部結構,只需不斷執行next()方法獲取下一個內容;
    2、不能隨機訪問集合中的某個值,只能從頭到尾順序的讀取;
    3、訪問到一半時不能回退,不能訪問之前的值;
    4、適合遍歷很大的資料集合,節省記憶體,提升速度。

    根據以上幾個特性應該對迭代器有所瞭解了吧,我這裡再舉一個例子便於加深理解,大家都玩過linux,在linux中有兩個命令,vi和cat,作用都是顯示出檔案的內容,而當一個檔案很大(比如1G)時,使用vi命令開啟檔案則明顯變慢,都有體會吧,會卡很久;而使用cat命令則沒有這個問題,無論多大的檔案,執行cat命令時都會馬上從檔案第一行資料開始列印;其實原因也很簡單,使用vi時需要將整個檔案載入到記憶體中再顯示出來,而使用cat時則從檔案第一行記錄開始逐行的讀取到記憶體中顯示,而已讀取過的內容則直接釋放掉,這樣每次讀取到記憶體中只有一行記錄響應速度當然快了。
    其實這裡的cat正是運用了迭代器的思想,迭代器每次順序取集合中的一個值到記憶體,用完即作廢,再取下一個值,對應特性1,對於很大的檔案遍歷則速度很快,對應特性4;則缺點也是明顯的,迭代器不能取集合中間某個值,對應特性2;前一個值讀取完即回收記憶體,所以無法重複讀取,對應特性3;講到這裡大家應該已經能充分理解迭代器的原理了,下面進行程式碼演示:
    1、建立一個迭代器:
    
a = iter(["wang","xuqian","xiaozhuzi"])    # 已建立一個迭代器物件,設定好需要迭代的值
    2、遍歷迭代器資料:
    
print(a.__next__())
    
print(a.__next__())
    
print(a.__next__())    # 有三個值,於是執行三次next()方法
    3、結果:
    wang
    xuqian
    xiaozhuzi
    就是這麼簡單,由於迭代器的特性,我們只能順序依次進行取值,不能像list那樣可以取集合中的任意值,在這裡三個值都取出後如果再執行a.__next__(),則會報錯:“已停止迭代”
    Traceback (most recent call last):
    print(a.__next__())
    StopIteration

二、生成器(Generator)和yield關鍵字
    生成器定義:當一個函式被呼叫時,返回一個迭代器,那麼這個函式就叫做生成器,如果函式中包括yield語法,則這個函式就是一個生成器
    yield:效果就是使函式中斷,並儲存中斷的狀態,中斷後,程式碼可以繼續往下執行,過一段時間還可以重新呼叫這個函式,並且可以從上次yield的下面的一句程式碼開始執行;yield可以返回值,也可以接收send來的引數。
    生成器和yield的解釋比較抽象難以理解,別急,下面透過除錯程式碼的方式展示他們的用法,以及yield的魅力!~
    
def cash(account):                # 先定義一個函式,接收一個引數
        
while account > 0:            # 當引數>0時
        
account -= 100                 #減去100
       
 yield 100                     #中斷函式嗎,返回100
       
 print("又來取錢啦!")        #列印
    根據生成器的定義,此時cash函式即為一個生成器,作為一個生成器,當被呼叫後產生的生成器物件肯定是支援迭代器介面,也就是生成器返回一個迭代器,下面我們進行驗證:
    
a = cash(500)        # 呼叫生成器,生成迭代器
    
print(a.__next__())    # 運用迭代器的next()方法列印第一個迭代值
     執行結果為:100
    以上驗證了呼叫生成器cash後的確生成了一個迭代器,關於迭代器應該沒有什麼疑問了,可以持續next()取值直到 account == 0;而大家現在一定有個疑問,這裡面的yield關鍵字的作用是什麼?為什麼列印結果沒有print("又來取錢啦!")
?那麼下面我跑一遍程式,透過結果來分析程式碼執行順序:
    
def cash(account):
        
while account > 0:
         
account -= 100
       
 yield 100
        
print("從我開始往下執行!") print("程式碼已經執行完啦")
    a = cash(500
    print("第0次執行我"
    print(a.__next__()) 
    print("第一次執行我"
    print(a.__next__())
執行結果為:     
    第0次執行我
    100
    第一次執行我
    從我開始往下執行!
    程式碼已經執行完啦!
    100
    
    從執行結果可以推斷出,當程式進行到
a = cash(500) 時,函式並沒有進行呼叫而是繼續往下走列印“第0次執行我”,為什麼?其實正是因為在函式定義的時候,檢測到函式中寫了yield關鍵字,此時這個函式就變成了一個生成器,於是函式暫停執行;當執行到第一個print(a.__next__()) 時,才開始真正的呼叫函式了,函式執行到yield 100 時,根據yield的特性,函式中斷返回100並列印,程式繼續向下執行列印“第一次執行我”;當執行到第二個print(a.__next__()) 時,繼續呼叫函式,根據yield的特性,會接著上一次中斷的位置繼續執行函式於是列印“從我開始往下執行!”“程式碼已經執行完啦”,而根據函式中的while迴圈,會再次執行迴圈程式碼,直到 yield 100,函式再次中斷返回100並列印。到此,程式執行完畢!

三、yield單執行緒非同步併發實現
    根據以上程式演示,大家應該對yield和生成器的特性理解更加深刻了吧,yield打破了常規的程式碼執行順序,甚至能跳出迴圈,而且下一次呼叫函式(生成器)時還可以接著上次的程式碼向下執行,是不是很牛X?那麼,yield的特性到底有什麼用呢?
    舉個例子,我去銀行取錢,加入取款額度較大,100萬~,銀行則需要稽核、調配資金等手續,假如需要1天的時間,那麼這一天我都要等在那裡,這顯然是不合理的吧。最好的辦法是我繼續做別的事情,等銀行審批結束後再通知我去取錢。對於我們寫的程式碼也是一樣,由於我們是單執行緒序列執行程式碼,當呼叫一個函式時,如果這個函式遲遲不返回結果怎麼辦?按照之前的思路,那麼程式只能卡死在那裡一直等待,就和去取錢等銀行審批一個道理;而yield的特性,打破了這一約束,使得我們在單執行緒程式設計的過程中,依然可以實現非同步併發!下面再看一段程式碼:
    
import time
    
def consumer(name):
        
print("%s 準備吃燒烤啦!" %name)
         
while True:
             
shaokao = yield
          
 print("燒烤%s被%s吃了!" %(shaokao,name))

    
def producter(name):
        
c = consumer("A")
        
c1 = consumer("B")
        
c.__next__()
        
c1.__next__()
        
print("開始生火烤串啦!")
        
for i in range(3):
            
time.sleep(1)
            
print("烤了兩串羊肉")
            
c.send(i)
            
c1.send(i)
producter("jiaqi")
根據前面講的知識,應該可以得出執行結果:
    A 準備吃燒烤啦!
    B 準備吃燒烤啦!
    開始生火烤串啦!
    烤了兩串羊肉
    燒烤0被A吃了!
    燒烤0被B吃了!
    烤了兩串羊肉
    燒烤1被A吃了!
    燒烤1被B吃了!
    烤了兩串羊肉
    燒烤2被A吃了!
    燒烤2被B吃了!
    
    這裡就實現了非同步併發控制,一個函式和生成器之間呼叫,透過yield實現;這裡面還有個知識點,c.send(i),前面講過yield不僅能返回值,而且還能接收值,在這裡send()大家可以理解為與next()一樣,都是觸發呼叫生成器中的程式碼,但next()可以理解為傳一個空值給yield,send()則可傳一個實際的值給yield。以上程式碼中將i值傳給了生成器consumer中的Yield



    


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/29018063/viewspace-2079767/,如需轉載,請註明出處,否則將追究法律責任。

相關文章