Python源碼剖析筆記3-Python執(zhí)行原理初探
之前寫了幾篇源碼剖析筆記纯路,然而慢慢覺得沒有從一個宏觀的角度理解python執(zhí)行原理的話姥份,從底向上分析未免太容易讓人疑惑凡伊,不如先從宏觀上對python執(zhí)行原理有了一個基本了解可帽,再慢慢探究細節(jié)钾军,這樣也許會好很多鳄袍。這也是最近這么久沒有更新了筆記了,一直在看源碼剖析書籍和源碼吏恭,希望能夠從一個宏觀層面理清python執(zhí)行原理拗小。人說讀書從薄讀厚,再從厚讀薄方是理解了真意樱哼,希望能夠達到這個境地吧哀九,加了個油。
1 Python運行環(huán)境初始化
在看怎么執(zhí)行之前搅幅,先要簡單的說明一下python的運行時環(huán)境初始化阅束。python中有一個解釋器狀態(tài)對象PyInterpreterState用于模擬進程(后面簡稱進程對象),另外有一個線程狀態(tài)對象PyThreadState模擬線程(后面簡稱線程對象)茄唐。python中的PyInterpreterState結(jié)構(gòu)通過一個鏈表鏈接起來息裸,用于模擬操作系統(tǒng)多進程。進程對象中有一個指針指向線程集合沪编,線程對象則有一個指針指向其對應(yīng)的進程對象呼盆,這樣線程和進程就關(guān)聯(lián)了起來。當(dāng)然漾抬,還少不了一個當(dāng)前運行線程對象_PyThreadState_Current用來維護當(dāng)前運行的線程宿亡。
1.1 進程線程初始化
python中調(diào)用PyInitialize()函數(shù)來完成運行環(huán)境初始化。在初始化函數(shù)中纳令,會創(chuàng)建進程對象interp以及線程對象并在進程對象和線程對象建立關(guān)聯(lián)挽荠,并設(shè)置當(dāng)前運行線程對象為剛創(chuàng)建的線程對象克胳。接下來是類型系統(tǒng)初始化,包括int圈匆,str漠另,bool,list等類型初始化跃赚,這里留到后面再慢慢分析笆搓。然后,就是另外一個大頭纬傲,那就是系統(tǒng)模塊初始化满败。進程對象interp中有一個modules變量用于維護所有的模塊對象,modules變量為字典對象,其中維護(name, module)對應(yīng)關(guān)系叹括,在python中對應(yīng)著sys.modules算墨。
1.2 模塊初始化
系統(tǒng)模塊初始化過程會初始化 __builtin__, sys, __main__, site
等模塊。在python中汁雷,模塊對象是以PyModuleObject結(jié)構(gòu)體存在的净嘀,除了通用的對象頭部,其中就只有一個字典字段md_dict侠讯。模塊對象中的md_dict字段存儲的內(nèi)容是我們很熟悉的,比如__name__, __doc__
等屬性挖藏,以及模塊中的方法等。
在__builtin__
模塊初始化中厢漩,md_dict中存儲的內(nèi)容就包括內(nèi)置函數(shù)以及系統(tǒng)類型對象膜眠,如len,dir,getattr等函數(shù)以及int,str,list等類型對象。正因為如此溜嗜,我們才能在代碼中直接用len函數(shù)柴底,因為根據(jù)LEGB規(guī)則,我們能夠在__builtin__
模塊中找到len這個符號粱胜。幾乎同樣的過程創(chuàng)建sys
模塊以及__main__
模塊柄驻。創(chuàng)建完成后,進程對象interp->builtins
會被設(shè)置為__builtin__
模塊的md_dict字段焙压,即模塊對象中的那個字典字段鸿脓。而interp->sysdict
則是被設(shè)置為sys模塊的md_dict字段。
sys模塊初始化后涯曲,其中包括前面提到過的modules以及path,version,stdin,stdout,maxint等屬性野哭,exit,getrefcount,_getframe等函數(shù)。注意這里是設(shè)置了基本的sys.path(即python安裝目錄的lib路徑等)幻件,第三方模塊的路徑是在site模塊初始化的時候添加的拨黔。
需要說明的是,__main__
模塊是個特殊的模塊绰沥,在我們寫第一個python程序時篱蝇,其中的__name__ == "__main__"
中的__main__
指的就是這個模塊名字贺待。當(dāng)我們用python xxx.py
運行python程序時,該源文件就可以當(dāng)作是名為__main__
的模塊了零截,而如果是通過其他模塊導(dǎo)入麸塞,則其名字就是源文件本身的名字,至于為什么涧衙,這個在后面運行一個python程序的例子中會詳細說明哪工。其中還有一點要說明的是,在創(chuàng)建__main__
模塊的時候弧哎,會在模塊的字典中插入("__builtins__", __builtin__ module)
對應(yīng)關(guān)系雁比。在后面可以看到這個模塊特別重要,因為在運行時棧幀對象PyFrameObject的f_buitins字段就會被設(shè)置為__builtin__
模塊撤嫩,而棧幀對象的locals和globals字段初始會被設(shè)置為__main__
模塊的字典章贞。
另外,site
模塊初始化主要用來初始化python第三方模塊搜索路徑非洲,我們經(jīng)常用的sys.path就是這個模塊設(shè)置的了。它不僅將site-packages路徑加到sys.path中蜕径,還會把site-packages目錄下面的.pth文件中的所有路徑加入到sys.path中两踏。
下面是一些驗證代碼,可以看到sys.modules中果然有了__builtin__, sys, __main__
等模塊兜喻。此外梦染,系統(tǒng)的類型對象都已經(jīng)位于__builtin__
模塊字典中。
In [13]: import sys
In [14]: sys.modules['__builtin__'].__dict__['int']
Out[14]: int
In [15]: sys.modules['__builtin__'].__dict__['len']
Out[15]: <function len>
In [16]: sys.modules['__builtin__'].__dict__['__name__']
Out[16]: '__builtin__'
In [17]: sys.modules['__builtin__'].__dict__['__doc__']
Out[17]: "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices."
In [18]: sys.modules['sys']
Out[18]: <module 'sys' (built-in)>
In [19]: sys.modules['__main__']
Out[19]: <module '__main__' (built-in)>
好了朴皆,基本工作已經(jīng)準備妥當(dāng)帕识,接下來可以運行python程序了。有兩種方式遂铡,一種是在命令行下面的交互肮疗,另外一種是以python xxx.py
的方式運行。在說明這兩種方式前扒接,需要先介紹下python程序運行相關(guān)的幾個結(jié)構(gòu)伪货。
1.3 Python運行相關(guān)數(shù)據(jù)結(jié)構(gòu)
python運行相關(guān)數(shù)據(jù)結(jié)構(gòu)主要由PyCodeObject,PyFrameObject以及PyFunctionObject钾怔。其中PyCodeObject是python字節(jié)碼的存儲結(jié)構(gòu)碱呼,編譯后的pyc文件就是以PyCodeObject結(jié)構(gòu)序列化后存儲的,運行時加載并反序列化為PyCodeObject對象宗侦。PyFrameObject是對棧幀的模擬愚臀,當(dāng)進入到一個新的函數(shù)時,都會有PyFrameObject對象用于模擬棧幀操作矾利。PyFunctionObject則是函數(shù)對象姑裂,一個函數(shù)對應(yīng)一個PyCodeObject,在執(zhí)行def test():
語句的時候會創(chuàng)建PyFunctionObject對象馋袜。可以這樣認為炭分,PyCodeObject是一種靜態(tài)的結(jié)構(gòu)桃焕,python源文件確定,那么編譯后的PyCodeObject對象也是不變的捧毛;而PyFrameObject和PyFunctionObject是動態(tài)結(jié)構(gòu)观堂,其中的內(nèi)容會在運行時動態(tài)變化。
PyCodeObject對象
python程序文件在執(zhí)行前需要編譯成PyCodeObject對象呀忧,每一個CodeBlock都會是一個PyCodeObject對象师痕,在Python中,類而账,函數(shù)胰坟,模塊都是一個Code Block,也就是說編譯后都有一個單獨的PyCodeObject對象泞辐,因此笔横,一個python文件編譯后可能會有多個PyCodeObject對象,比如下面的示例程序編譯后就會存在2個PyCodeObject對象咐吼,一個對應(yīng)test.py整個文件吹缔,一個對應(yīng)函數(shù)test。關(guān)于PyCodeObject對象的解析锯茄,可以參見我之前的文章Python pyc格式解析厢塘,這里就不贅述了。
#示例代碼test.py
def test():
print "hello world"
if __name__ == "__main__":
test()
PyFrameObject對象
python程序的字節(jié)碼指令以及一些靜態(tài)信息比如常量等都存儲在PyCodeObject中肌幽,運行時顯然不可能只是操作PyCodeObject對象晚碾,因為有很多內(nèi)容是運行時動態(tài)改變的,比如下面這個代碼test2.py,雖然1和2處的字節(jié)碼指令相同喂急,但是它們執(zhí)行結(jié)果顯然是不同的格嘁,這些信息顯然不能在PyCodeObject中存儲,這些信息其實需要通過PyFrameObject也就是棧幀對象來獲取廊移。PyFrameObject對象中有l(wèi)ocals讥蔽,globals,builtins三個字段對應(yīng)local画机,global冶伞,builtin三個名字空間,即我們常說的LGB規(guī)則步氏,當(dāng)然加上閉包响禽,就是LEGB規(guī)則。一個模塊對應(yīng)的文件定義一個global作用域,一個函數(shù)定義一個local作用域芋类,python自身定義了一個頂級作用域builtin作用域隆嗅,這三個作用域分別對應(yīng)PyFrameObject對象的三個字段,這樣就可以找到對應(yīng)的名字引用侯繁。比如test2.py中的1處的i引用的是函數(shù)test的局部變量i胖喳,對應(yīng)內(nèi)容是字符串“hello world”,而2處的i引用的是模塊的local作用域的名字i贮竟,對應(yīng)內(nèi)容是整數(shù)123(注意模塊的local作用域和global作用域是一樣的)丽焊。需要注意的是,函數(shù)中局部變量的訪問并不需要去訪問locals名字空間咕别,因為函數(shù)的局部變量總是不變的技健,在編譯時就能確定局部變量使用的內(nèi)存位置。
#示例代碼test2.py
i = 123
def test():
i = 'hello world'
print i #1
test()
print i #2
PyFunctionObject對象
PyFunctionObject是函數(shù)對象惰拱,在創(chuàng)建函數(shù)的指令MAKE_FUNCTION中構(gòu)建雌贱。PyFunctionObject中有個func_code字段指向該函數(shù)對應(yīng)的PyCodeObject對象,另外還有func_globals指向global名字空間偿短,注意到這里并沒有使用local名字空間欣孤。調(diào)用函數(shù)時,會創(chuàng)建新的棧幀對象PyFrameObject來執(zhí)行函數(shù)昔逗,函數(shù)調(diào)用關(guān)系通過棧幀對象PyFrameObject中的f_back字段進行關(guān)聯(lián)降传。最終執(zhí)行函數(shù)調(diào)用時,PyFunctionObject對象的影響已經(jīng)消失纤子,真正起作用的是PyFunctionObject的PyCodeObject對象和global名字空間,因為在創(chuàng)建函數(shù)棧幀時會將這兩個參數(shù)傳給PyFrameObject對象款票。
1.4 Python程序運行過程淺析
說完幾個基本對象控硼,現(xiàn)在回到之前的話題,開始準備執(zhí)行python程序艾少。兩種方式交互式和直接python xxx.py
雖然有所不同卡乾,但最終歸于一處,就是啟動虛擬機執(zhí)行python字節(jié)碼缚够。這里以python xxx.py
方式為例幔妨,在運行python程序之前,需要對源文件編譯成字節(jié)碼谍椅,創(chuàng)建PyCodeObject對象误堡。這個是通過PyAST_Compile函數(shù)實現(xiàn)的,至于具體編譯流程雏吭,這就要參看《編譯原理》那本龍書了锁施,這里暫時當(dāng)做黑盒好了,因為單就編譯這部分而言,一時半會也說不清楚(好吧悉抵,其實是我也沒有學(xué)好編譯原理)肩狂。編譯后得到PyCodeObject對象,然后調(diào)用PyEval_EvalCode(co, globals, locals)
函數(shù)創(chuàng)建PyFrameObject對象并執(zhí)行字節(jié)碼了姥饰。注意到參數(shù)里面的co是PyCodeObject對象傻谁,而由于運行PyEval_EvalCode時創(chuàng)建的棧幀對象是Python創(chuàng)建的第一個PyFrameObject對象,所以f_back為NULL列粪,而且它的globals和locals就是__main__
模塊的字典對象审磁。如果我們不是直接運行,而是導(dǎo)入一個模塊的話篱竭,則還會將python源碼編譯后得到的PyCodeObject對象保存到pyc文件中力图,下次加載模塊時如果這個模塊沒有改動過就可以直接從pyc文件中讀取內(nèi)容而不需要再次編譯了。
執(zhí)行字節(jié)碼的過程就是模擬CPU執(zhí)行指令的過程一樣掺逼,先指向PyFrameObject的f_code字段對應(yīng)的PyCodeObject對象的co_code字段吃媒,這就是字節(jié)碼存儲的位置,然后取出第一條指令吕喘,接著第二條指令...依次執(zhí)行完所有的指令赘那。python中指令長度為1個字節(jié)或者3個字節(jié),其中無參數(shù)的指令長度是1個字節(jié)氯质,有參數(shù)的指令長度是3個字節(jié)(指令1字節(jié)+參數(shù)2字節(jié))募舟。
python虛擬機的進程,線程闻察,棧幀對象等關(guān)系如下圖所示:
2 Python程序運行實例說明
程序猿學(xué)習(xí)一門新的語言往往都是從hello world開始的拱礁,一來就跟世界打個招呼,因為接下來就要去面對程序語言未知的世界了辕漂。我學(xué)習(xí)python也是從這里開始的呢灶,只是以前并不去深究它的執(zhí)行原理,這回是逃不過去了钉嘹⊙炷耍看看下面的栗子。
#示例代碼test3.py
i = 1
s = 'hello world'
def test():
k = 5
print k
print s
if __name__ == "__main__":
test()
這個例子代碼不多跋涣,不過也涉及到python運行原理的方方面面(除了類機制那一塊外缨睡,類機制那一塊還沒有理清楚,先不理會)陈辱。那么按照之前部分說的奖年,執(zhí)行python test3.py
的時候,會先初始化python進程和線程沛贪,然后初始化系統(tǒng)模塊以及類型系統(tǒng)等拾并,然后運行python程序test3.py揍堰。每次運行python程序都是開啟一個python虛擬機,由于是直接運行嗅义,需要先編譯為字節(jié)碼格式屏歹,得到PyCodeObject對象,然后從字節(jié)碼對象的第一條指令開始執(zhí)行之碗。因為是直接運行蝙眶,所以PyCodeObject也就沒有序列化到pyc文件保存了。下面可以看下test3.py的PyCodeObject褪那,使用python的dis模塊可以看到字節(jié)碼指令幽纷。
In [1]: source = open('test3.py').read()
In [2]: co = compile(source, 'test3.py', 'exec')
In [3]: co.co_consts
Out[3]:
(1,
'hello world',
<code object test at 0x1108eaaf8, file "run.py", line 4>,
'__main__',
None)
In [4]: co.co_names
Out[4]: ('i', 's', 'test', '__name__')
In [5]: dis.dis(co) ##模塊本身的字節(jié)碼,下面說的整數(shù)博敬,字符串等都是指python中的對象友浸,對應(yīng)PyIntObject,PyStringObject等偏窝。
1 0 LOAD_CONST 0 (1) # 加載常量表中的第0個常量也就是整數(shù)1到棧中收恢。
3 STORE_NAME 0 (i) # 獲取變量名i,出棧剛剛加載的整數(shù)1祭往,然后存儲變量名和整數(shù)1到f->f_locals中伦意,這個字段對應(yīng)著查找名字時的local名字空間。
2 6 LOAD_CONST 1 ('hello world')
9 STORE_NAME 1 (s) #同理硼补,獲取變量名s驮肉,出棧剛剛加載的字符串hello world,并存儲變量名和字符串hello world的對應(yīng)關(guān)系到local名字空間已骇。
4 12 LOAD_CONST 2 (<code object test at 0xb744bd10, file "test3.py", line 4>)
15 MAKE_FUNCTION 0 #出棧剛剛?cè)霔5暮瘮?shù)test的PyCodeObject對象离钝,以code object和PyFrameObject的f_globals為參數(shù)創(chuàng)建函數(shù)對象PyFunctionObject并入棧
18 STORE_NAME 2 (test) #獲取變量test,并出棧剛?cè)霔5腜yFunctionObject對象褪储,并存儲到local名字空間卵渴。
9 21 LOAD_NAME 3 (__name__) ##LOAD_NAME會先依次搜索local,global乱豆,builtin名字空間奖恰,當(dāng)然我們這里是在local名字空間能找到__name__吊趾。
24 LOAD_CONST 3 ('__main__')
27 COMPARE_OP 2 (==) ##比較指令
30 JUMP_IF_FALSE 11 (to 44) ##如果不相等則直接跳轉(zhuǎn)到44對應(yīng)的指令處宛裕,也就是下面的POP_TOP。因為在COMPARE_OP指令中论泛,會設(shè)置棧頂為比較的結(jié)果揩尸,所以需要出棧這個比較結(jié)果。當(dāng)然我們這里是相等屁奏,所以接著往下執(zhí)行33處的指令岩榆,也是POP_TOP。
33 POP_TOP
10 34 LOAD_NAME 2 (test) ##加載函數(shù)對象
37 CALL_FUNCTION 0 ##調(diào)用函數(shù)
40 POP_TOP ##出棧函數(shù)返回值
41 JUMP_FORWARD 1 (to 45) ##前進1步,注意是下一條指令地址+1勇边,也就是44+1=45
>> 44 POP_TOP
>> 45 LOAD_CONST 4 (None)
48 RETURN_VALUE #返回None
In [6]: dis.dis(co.co_consts[2]) ##查看函數(shù)test的字節(jié)碼
5 0 LOAD_CONST 1 (5)
3 STORE_FAST 0 (k) #STORE_FAST與STORE_NAME不同犹撒,它是存儲到PyFrameObject的f_localsplus中,不是local名字空間粒褒。
6 6 LOAD_FAST 0 (k) #相對應(yīng)的识颊,LOAD_FAST是從f_localsplus取值
9 PRINT_ITEM
10 PRINT_NEWLINE #打印輸出
7 11 LOAD_GLOBAL 0 (s) #因為函數(shù)沒有使用local名字空間,所以奕坟,這里不是LOAD_NAME,而是LOAD_GLOBAL祥款,不要被名字迷惑,它實際上會依次搜索global月杉,builtin名字空間刃跛。
14 PRINT_ITEM
15 PRINT_NEWLINE
16 LOAD_CONST 0 (None)
19 RETURN_VALUE
按照我們前面的分析,test3.py這個文件編譯后其實對應(yīng)2個PyCodeObject苛萎,一個是本身test3.py這個模塊整體的PyCodeObject桨昙,另外一個則是函數(shù)test對應(yīng)的PyCodeObject。根據(jù)PyCodeObject的結(jié)構(gòu)首懈,我們可以知道test3.py字節(jié)碼中常量co_consts有5個绊率,分別是整數(shù)1,字符串‘hello world'究履,函數(shù)test對應(yīng)的PyCodeObject對象滤否,字符串__main__
,以及模塊返回值None對象最仑。恩藐俺,從這里可以發(fā)現(xiàn),其實模塊也是有返回值的泥彤。我們同樣可以用dis模塊查看函數(shù)test的字節(jié)碼欲芹。
關(guān)于字節(jié)碼指令,代碼中做了解析吟吝。需要注意到函數(shù)中局部變量如k的取值用的是LOAD_FAST菱父,即直接從PyFrameObject的f_localsplus字段取,而不是LOAD_NAME那樣依次從local剑逃,global以及builtin查找浙宜,這是函數(shù)的特性決定的。函數(shù)的運行時棧也是位于f_localsplus對應(yīng)的那片內(nèi)存中蛹磺,只是前面一部分用于存儲函數(shù)參數(shù)和局部變量粟瞬,而后面那部分才是運行時棧使用,這樣邏輯上運行時棧和函數(shù)參數(shù)以及局部變量是分離的萤捆,雖然物理上它們是連在一起的裙品。需要注意的是俗批,python中使用了預(yù)測指令機制,比如COMPARE_OP經(jīng)常跟JUMP_IF_FALSE或JUMP_IF_TRUE成對出現(xiàn)市怎,所以如果COMPARE_OP的下一條指令正好是JUNP_IF_FALSE岁忘,則可以直接跳轉(zhuǎn)到對應(yīng)代碼處執(zhí)行,提高一定效率区匠。
此外臭觉,還要知道在運行test3.py的時候,模塊的test3.py棧幀對象中的f_locals和f_globals的值是一樣的辱志,都是__main__
模塊的字典蝠筑。在test3.py的代碼后面加上如下代碼可以驗證這個猜想。
... #test3.py的代碼
if __name__ == "__main__":
test()
print locals() == sys.modules['__main__'].__dict__ # True
print globals() == sys.modules['__main__'].__dict__ # True
print globals() == locals() # True
正式因為如此揩懒,所以python中函數(shù)定義順序是無關(guān)的什乙,不需要跟C語言那樣在調(diào)用函數(shù)前先聲明函數(shù)。比如下面test4.py是完全正常的代碼已球,函數(shù)定義順序不影響函數(shù)調(diào)用臣镣,因為在執(zhí)行def語句的時候,會執(zhí)行MAKE_FUNCTION指令將函數(shù)對象加入到local名字空間智亮,而local和global此時對應(yīng)的是同一個字典忆某,所以也相當(dāng)于加入了global名字空間,從而在運行函數(shù)g的時候是可以找到函數(shù)f的阔蛉。另外也可以注意到弃舒,函數(shù)聲明和實現(xiàn)其實是分離的,聲明的字節(jié)碼指令在模塊的PyCodeObject中執(zhí)行状原,而實現(xiàn)的字節(jié)碼指令則是在函數(shù)自己的PyCodeObject中聋呢。
#test4.py
def g():
print 'function g'
f()
def f():
print 'function f'
g()
~