1. 程式人生 > >玩轉python(5)生成器的原理

玩轉python(5)生成器的原理

turn eval python解釋器 生命周期 固定 eva AR 返回 函數的參數

函數的調用滿足“後進先出”的原則,也就是說,最後被調用的函數應該第一個返回,函數的遞歸調用就是一個經典的例子。顯然,內存中以“後進先出”方式處理數據的棧段是最適合用於實現函數調用的載體,在編譯型程序語言中,函數被調用後,函數的參數,返回地址,寄存器值等數據會被壓入棧,待函數體執行完畢,將上述數據彈出棧。這也意味著,一個被調用的函數一旦執行完畢,它的生命周期就結束了。

在python這樣的解釋型語言中,函數的調用也是依賴棧的。之前說過,python的標準解釋器是用C寫的。解釋器用一個叫做PyEval_EvalFrameEx的C函數來執行python程序。對於一個python中的函數,解釋器接受一個python的棧幀對象,並在這個棧幀的上下文中執行python字節碼。

我們來看看這樣一個例子:

import dis

def foo():
    bar()

def bar():
    pass
    
print(dis.dis(foo))

我們用dis模塊可以查看python程序的字節碼,下面是函數foo()的字節碼:

0 LOAD_GLOBAL              0 (bar)
2 CALL_FUNCTION            0
4 POP_TOP
6 LOAD_CONST               0 (None)
8 RETURN_VALUE

foo函數將bar加載到棧中並調用它,然後從棧中彈出返回值,最後加載並返回None,當PyEval_EvalFrameEx遇到CALL_FUNCTION字節碼的時候,它會創建一個新的python棧幀,然後用這個新的幀作為參數遞歸調用PyEval_EvalFrameEx來執行bar。不過有一點要註意的是,python解釋器是個普通的C程序,所以它的堆棧幀就是普通的堆棧。但是它操作的python堆棧幀是分配在堆上的,所以python的棧幀可以在它的調用之外存活。而且可以顯式的保存下來。

這是python生成器的技術基礎,下面是一個生成器:

def gen():
    yield 1
    yield 2

g = gen()
next(g)

print(type(g))

返回的結果是:

<class ‘generator‘>

調用gen()產生的所有生成器都指向同一個代碼對象,但是每個都有自己的堆棧幀。這個堆棧幀並不存在於實際的堆棧上,它在堆內存上等待著被使用。

技術分享圖片

堆棧幀有個“last instruction”指針,指向最近執行的那條指令。剛開始的時候last instruction指針是-1意味著生成器尚未開始,這就是為什麽上面的例子中有next(g)這行代碼。生成器可以在任何時候被任何函數恢復執行,因為它的棧幀實際上不在棧上而是在堆上。生成器在調用調用層次結構中的位置不是固定的,也不需要遵循常規函數執行時遵循的先進後出順序。因為這些特性,生成器不僅能用於生成可叠代對象,還可以用於實現多任務協作。下一篇博文我會介紹python中一個很重要的概念:協程。

玩轉python(5)生成器的原理