天天看點

Python yield與實作

<code>yield</code>的功能類似于<code>return</code>,但是不同之處在于它傳回的是<code>生成器</code>。

生成器是通過一個或多個<code>yield</code>表達式構成的函數,每一個生成器都是一個疊代器(但是疊代器不一定是生成器)。

如果一個函數包含<code>yield</code>關鍵字,這個函數就會變為一個生成器。

生成器并不會一次傳回所有結果,而是每次遇到<code>yield</code>關鍵字後傳回相應結果,并保留函數目前的運作狀态,等待下一次的調用。

由于生成器也是一個疊代器,那麼它就應該支援<code>next</code>方法來擷取下一個值。

除了<code>next</code>函數,生成器還支援<code>send</code>函數。該函數可以向生成器傳遞參數。

最經典的例子,生成無限序列。

正常的解決方法是,生成一個滿足要求的很大的清單,這個清單需要儲存在記憶體中,很明顯記憶體限制了這個問題。

如果使用生成器就不需要傳回整個清單,每次都隻是傳回一個資料,避免了記憶體的限制問題。

生成器的源碼在<code>objects/genobject.c</code>。

在解釋生成器之前,需要講解一下python虛拟機的調用原理。

python虛拟機有一個棧幀的調用棧,其中棧幀的是<code>pyframeobject</code>,位于<code>include/frameobject.h</code>。

棧幀儲存了給出代碼的的資訊和上下文,其中包含最後執行的指令,全局和局部命名空間,異常狀态等資訊。<code>f_valueblock</code>儲存了資料,<code>b_blockstack</code>儲存了異常和循環控制方法。

舉一個例子來說明,

那麼,相應的調用棧如下,一個py檔案,一個類,一個函數都是一個代碼塊,對應者一個frame,儲存着上下文環境以及位元組碼指令。

每一個棧幀都擁有自己的資料棧和block棧,獨立的資料棧和block棧使得解釋器可以中斷和恢複棧幀(生成器正式利用這點)。

python代碼首先被編譯為位元組碼,再由python虛拟機來執行。一般來說,一條python語句對應着多條位元組碼(由于每條位元組碼對應着一條c語句,而不是一個機器指令,是以不能按照位元組碼的數量來判斷代碼性能)。

調用<code>dis</code>子產品可以分析位元組碼,

其中,

由了上面對于調用棧的了解,就可以很容易的明白生成器的具體實作。

生成器的源碼位于<code>object/genobject.c</code>。

<code>next</code>與<code>send</code>函數,如下

從上面的代碼中可以看到,<code>send</code>和<code>next</code>都是調用的同一函數<code>gen_send_ex</code>,差別在于是否帶有參數。

<code>pyeval_evalframeex</code>函數的功能為執行位元組碼并傳回結果。

舉一個例子,<code>f_back</code>上一個frame,<code>f_lasti</code>上一次執行的指令的偏移量,

結果如下,其中第三行的英文為操作碼,對應着上面的<code>opcode</code>,每次switch都是在不同的<code>opcode</code>之間進行選擇。

Python yield與實作