Python源碼剖析筆記3-Python執行原理初探

來源:互聯網
上載者:User

標籤:python   源碼   python執行原理   

Python源碼剖析筆記3-Python執行原理初探

本文簡書地址:http://www.jianshu.com/p/03af86845c95

之前寫了幾篇源碼剖析筆記,然而慢慢覺得沒有從一個宏觀的角度理解python執行原理的話,從底向上分析未免太容易讓人疑惑,不如先從宏觀上對python執行原理有了一個基本瞭解,再慢慢探究細節,這樣也許會好很多。這也是最近這麼久沒有更新了筆記了,一直在看源碼剖析書籍和源碼,希望能夠從一個宏觀層面理清python執行原理。人說讀書從薄讀厚,再從厚讀薄方是理解了真意,希望能夠達到這個境地吧,加了個油。

1 Python運行環境初始化

在看怎麼執行之前,先要簡單的說明一下python的運行時環境初始化。python中有一個解譯器狀態物件PyInterpreterState用於類比進程(後面簡稱進程對象),另外有一個線程狀態物件PyThreadState類比線程(後面簡稱線程對象)。python中的PyInterpreterState結構通過一個鏈錶鏈接起來,用於類比作業系統多進程。進程對象中有一個指標指向線程集合,線程對象則有一個指標指向其對應的進程對象,這樣線程和進程就關聯了起來。當然,還少不了一個當前運行線程對象_PyThreadState_Current用來維護當前啟動並執行線程。

1.1 進程線程初始化

python中調用PyInitialize()函數來完成運行環境初始化。在初始化函數中,會建立進程對象interp以及線程對象並在進程對象和線程對象建立關聯,並設定當前運行線程對象為剛建立的線程對象。接下來是類型系統初始化,包括int,str,bool,list等類型初始化,這裡留到後面再慢慢分析。然後,就是另外一個大頭,那就是系統模組初始化。進程對象interp中有一個modules變數用於維護所有的模組對象,modules變數為字典對象,其中維護(name, module)對應關係,在python中對應著sys.modules。

1.2 模組初始化

系統模組初始化過程會初始化 __builtin__, sys, __main__, site等模組。在python中,模組對象是以PyModuleObject結構體存在的,除了通用的對象頭部,其中就只有一個字典欄位md_dict。模組對象中的md_dict欄位儲存的內容是我們很熟悉的,比如__name__, __doc__等屬性,以及模組中的方法等。

__builtin__模組初始化中,md_dict中儲存的內容就包括內建函數以及系統類別型對象,如len,dir,getattr等函數以及int,str,list等類型對象。正因為如此,我們才能在代碼中直接用len函數,因為根據LEGB規則,我們能夠在__builtin__模組中找到len這個符號。幾乎同樣的過程建立sys模組以及__main__模組。建立完成後,進程對象interp->builtins會被設定為__builtin__模組的md_dict欄位,即模組對象中的那個字典欄位。而interp->sysdict則是被設定為sys模組的md_dict欄位。

sys模組初始化後,其中包括前面提到過的modules以及path,version,stdin,stdout,maxint等屬性,exit,getrefcount,_getframe等函數。注意這裡是設定了基本的sys.path(即python安裝目錄的lib路徑等),第三方模組的路徑是在site模組初始化的時候添加的。

需要說明的是,__main__模組是個特殊的模組,在我們寫第一個python程式時,其中的__name__ == "__main__"中的__main__指的就是這個模組名字。當我們用python xxx.py運行python程式時,該源檔案就可以當作是名為__main__的模組了,而如果是通過其他模組匯入,則其名字就是源檔案本身的名字,至於為什麼,這個在後面運行一個python程式的例子中會詳細說明。其中還有一點要說明的是,在建立__main__模組的時候,會在模組的字典中插入("__builtins__", __builtin__ module)對應關係。在後面可以看到這個模組特別重要,因為在運行時棧幀對象PyFrameObject的f_buitins欄位就會被設定為__builtin__模組,而棧幀對象的locals和globals欄位初始會被設定為__main__模組的字典。

另外,site模組初始化主要用來初始化python第三方模組搜尋路徑,我們經常用的sys.path就是這個模組設定的了。它不僅將site-packages路徑加到sys.path中,還會把site-packages目錄下面的.pth檔案中的所有路徑加入到sys.path中。

下面是一些驗證代碼,可以看到sys.modules中果然有了__builtin__, sys, __main__等模組。此外,系統的類型對象都已經位於__builtin__模組字典中。

In [13]: import sysIn [14]: sys.modules[‘__builtin__‘].__dict__[‘int‘]Out[14]: intIn [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)>

好了,基本工作已經準備妥當,接下來可以運行python程式了。有兩種方式,一種是在命令列下面的互動,另外一種是以python xxx.py的方式運行。在說明這兩種方式前,需要先介紹下python程式運行相關的幾個結構。

1.3 Python運行相關資料結構

python運行相關資料結構主要由PyCodeObject,PyFrameObject以及PyFunctionObject。其中PyCodeObject是python位元組碼的儲存結構,編譯後的pyc檔案就是以PyCodeObject結構序列化後儲存的,運行時載入並還原序列化為PyCodeObject對象。PyFrameObject是對棧幀的類比,當進入到一個新的函數時,都會有PyFrameObject對象用於類比棧幀操作。PyFunctionObject則是函數對象,一個函數對應一個PyCodeObject,在執行def test():語句的時候會建立PyFunctionObject對象。可以這樣認為,PyCodeObject是一種靜態結構,python源檔案確定,那麼編譯後的PyCodeObject對象也是不變的;而PyFrameObject和PyFunctionObject是動態結構,其中的內容會在運行時動態變化。

PyCodeObject對象

python程式檔案在執行前需要編譯成PyCodeObject對象,每一個CodeBlock都會是一個PyCodeObject對象,在Python中,類,函數,模組都是一個Code Block,也就是說編譯後都有一個單獨的PyCodeObject對象,因此,一個python檔案編譯後可能會有多個PyCodeObject對象,比如下面的樣本程式編譯後就會存在2個PyCodeObject對象,一個對應test.py整個檔案,一個對應函數test。關於PyCodeObject對象的解析,可以參見我之前的文章Python pyc格式解析,這裡就不贅述了。

#範例程式碼test.pydef test():    print "hello world"if __name__ == "__main__":    test()
PyFrameObject對象

python程式的位元組碼指令以及一些靜態資訊比如常量等都儲存在PyCodeObject中,運行時顯然不可能只是操作PyCodeObject對象,因為有很多內容是運行時動態改變的,比如下面這個代碼test2.py,雖然1和2處的位元組碼指令相同,但是它們執行結果顯然是不同的,這些資訊顯然不能在PyCodeObject中儲存,這些資訊其實需要通過PyFrameObject也就是棧幀對象來擷取。PyFrameObject對象中有locals,globals,builtins三個欄位對應local,global,builtin三個名字空間,即我們常說的LGB規則,當然加上閉包,就是LEGB規則。一個模組對應的檔案定義一個global範圍,一個函數定義一個local範圍,python自身定義了一個頂級範圍builtin範圍,這三個範圍分別對應PyFrameObject對象的三個欄位,這樣就可以找到對應的名字引用。比如test2.py中的1處的i引用的是函數test的局部變數i,對應內容是字串“hello world”,而2處的i引用的是模組的local範圍的名字i,對應內容是整數123(注意模組的local範圍和global範圍是一樣的)。需要注意的是,函數中局部變數的訪問並不需要去訪問locals名字空間,因為函數的局部變數總是不變的,在編譯時間就能確定局部變數使用的記憶體位置。

#範例程式碼test2.pyi = 123                                                                                                                                                       def test():  i = ‘hello world‘  print i #1test()print i #2
PyFunctionObject對象

PyFunctionObject是函數對象,在建立函數的指令MAKE_FUNCTION中構建。PyFunctionObject中有個func_code欄位指向該函數對應的PyCodeObject對象,另外還有func_globals指向global名字空間,注意到這裡並沒有使用local名字空間。調用函數時,會建立新的棧幀對象PyFrameObject來執行函數,函數調用關係通過棧幀對象PyFrameObject中的f_back欄位進行關聯。最終執行函數調用時,PyFunctionObject對象的影響已經消失,真正起作用的是PyFunctionObject的PyCodeObject對象和global名字空間,因為在建立函數棧幀時會將這兩個參數傳給PyFrameObject對象。

1.4 Python程式運行過程淺析

說完幾個基本對象,現在回到之前的話題,開始準備執行python程式。兩種方式互動式和直接python xxx.py雖然有所不同,但最終歸於一處,就是啟動虛擬機器執行python位元組碼。這裡以python xxx.py方式為例,在運行python程式之前,需要對源檔案編譯成位元組碼,建立PyCodeObject對象。這個是通過PyAST_Compile函數實現的,至於具體編譯流程,這就要參看《編譯原理》那本龍書了,這裡暫時當做黑盒好了,因為單就編譯這部分而言,一時半會也說不清楚(好吧,其實是我也沒有學好編譯原理)。編譯後得到PyCodeObject對象,然後調用PyEval_EvalCode(co, globals, locals)函數建立PyFrameObject對象並執行位元組碼了。注意到參數裡面的co是PyCodeObject對象,而由於運行PyEval_EvalCode時建立的棧幀對象是Python建立的第一個PyFrameObject對象,所以f_back為NULL,而且它的globals和locals就是__main__模組的字典對象。如果我們不是直接運行,而是匯入一個模組的話,則還會將python源碼編譯後得到的PyCodeObject對象儲存到pyc檔案中,下次載入模組時如果這個模組沒有改動過就可以直接從pyc檔案中讀取內容而不需要再次編譯了。

執行位元組碼的過程就是類比CPU執行指令的過程一樣,先指向PyFrameObject的f_code欄位對應的PyCodeObject對象的co_code欄位,這就是位元組碼儲存的位置,然後取出第一條指令,接著第二條指令…依次執行完所有的指令。python中指令長度為1個位元組或者3個位元組,其中無參數的指令長度是1個位元組,有參數的指令長度是3個位元組(指令1位元組+參數2位元組)。

python虛擬機器的進程,線程,棧幀對象等關係如所示:

2 Python程式運行執行個體說明

程式猿學習一門新的語言往往都是從hello world開始的,一來就跟世界打個招呼,因為接下來就要去面對程式語言未知的世界了。我學習python也是從這裡開始的,只是以前並不去深究它的執行原理,這回是逃不過去了。看看下面的栗子。

#範例程式碼test3.pyi = 1s = ‘hello world‘def test():    k = 5    print k    print sif __name__ == "__main__":    test()

這個例子代碼不多,不過也涉及到python運行原理的方方面面(除了類機制那一塊外,類機制那一塊還沒有理清楚,先不理會)。那麼按照之前部分說的,執行python test3.py的時候,會先初始化python進程和線程,然後初始化系統模組以及類型系統等,然後運行python程式test3.py。每次運行python程式都是開啟一個python虛擬機器,由於是直接運行,需要先編譯為位元組碼格式,得到PyCodeObject對象,然後從位元組碼對象的第一條指令開始執行。因為是直接運行,所以PyCodeObject也就沒有序列化到pyc檔案儲存了。下面可以看下test3.py的PyCodeObject,使用python的dis模組可以看到位元組碼指令。

In [1]: source = open(‘test3.py‘).read()In [2]: co = compile(source, ‘test3.py‘, ‘exec‘)In [3]: co.co_constsOut[3]: (1, ‘hello world‘, <code object test at 0x1108eaaf8, file "run.py", line 4>, ‘__main__‘, None)In [4]: co.co_namesOut[4]: (‘i‘, ‘s‘, ‘test‘, ‘__name__‘)In [5]: dis.dis(co) ##模組本身的位元組碼,下面說的整數,字串等都是指python中的對象,對應PyIntObject,PyStringObject等。  1           0 LOAD_CONST               0 (1) # 載入常量表中的第0個常量也就是整數1到棧中。              3 STORE_NAME               0 (i) # 擷取變數名i,出棧剛剛載入的整數1,然後儲存變數名和整數1到f->f_locals中,這個欄位對應著尋找名字時的local名字空間。  2           6 LOAD_CONST               1 (‘hello world‘)               9 STORE_NAME               1 (s)  #同理,擷取變數名s,出棧剛剛載入的字串hello world,並儲存變數名和字串hello world的對應關係到local名字空間。  4          12 LOAD_CONST               2 (<code object test at 0xb744bd10, file "test3.py", line 4>)             15 MAKE_FUNCTION            0   #出棧剛剛入棧的函數test的PyCodeObject對象,以code object和PyFrameObject的f_globals為參數建立函數對象PyFunctionObject併入棧             18 STORE_NAME               2 (test)  #擷取變數test,並出棧剛入棧的PyFunctionObject對象,並儲存到local名字空間。  9          21 LOAD_NAME                3 (__name__) ##LOAD_NAME會先依次搜尋local,global,builtin名字空間,當然我們這裡是在local名字空間能找到__name__。             24 LOAD_CONST               3 (‘__main__‘)             27 COMPARE_OP               2 (==)  ##比較指令             30 JUMP_IF_FALSE           11 (to 44) ##如果不相等則直接跳轉到44對應的指令處,也就是下面的POP_TOP。因為在COMPARE_OP指令中,會設定棧頂為比較的結果,所以需要出棧這個比較結果。當然我們這裡是相等,所以接著往下執行33處的指令,也是POP_TOP。             33 POP_TOP              10          34 LOAD_NAME                2 (test) ##載入函數對象             37 CALL_FUNCTION            0  ##調用函數             40 POP_TOP                     ##出棧函數傳回值             41 JUMP_FORWARD             1 (to 45) ##前進1步,注意是下一條指令地址+1,也就是44+1=45        >>   44 POP_TOP                     >>   45 LOAD_CONST               4 (None)              48 RETURN_VALUE     #返回NoneIn [6]: dis.dis(co.co_consts[2])  ##查看函數test的位元組碼  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) #相對應的,LOAD_FAST是從f_localsplus取值              9 PRINT_ITEM                       10 PRINT_NEWLINE         #列印輸出   7          11 LOAD_GLOBAL              0 (s) #因為函數沒有使用local名字空間,所以,這裡不是LOAD_NAME,而是LOAD_GLOBAL,不要被名字迷惑,它實際上會依次搜尋global,builtin名字空間。             14 PRINT_ITEM                       15 PRINT_NEWLINE                    16 LOAD_CONST               0 (None)             19 RETURN_VALUE        

按照我們前面的分析,test3.py這個檔案編譯後其實對應2個PyCodeObject,一個是本身test3.py這個模組整體的PyCodeObject,另外一個則是函數test對應的PyCodeObject。根據PyCodeObject的結構,我們可以知道test3.py位元組碼中常量co_consts有5個,分別是整數1,字串‘hello world’,函數test對應的PyCodeObject對象,字串__main__,以及模組傳回值None對象。恩,從這裡可以發現,其實模組也是有傳回值的。我們同樣可以用dis模組查看函數test的位元組碼。

關於位元組碼指令,代碼中做瞭解析。需要注意到函數中局部變數如k的取值用的是LOAD_FAST,即直接從PyFrameObject的f_localsplus欄位取,而不是LOAD_NAME那樣依次從local,global以及builtin尋找,這是函數的特性決定的。函數的運行時棧也是位於f_localsplus對應的那片記憶體中,只是前面一部分用於儲存函數參數和局部變數,而後面那部分才是運行時棧使用,這樣邏輯上運行時棧和函數參數以及局部變數是分離的,雖然物理上它們是連在一起的。需要注意的是,python中使用了預測指令機制,比如COMPARE_OP經常跟JUMP_IF_FALSE或JUMP_IF_TRUE成對出現,所以如果COMPARE_OP的下一條指令正好是JUNP_IF_FALSE,則可以直接跳轉到對應代碼處執行,提高一定效率。

此外,還要知道在運行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中函數定義順序是無關的,不需要跟C語言那樣在調用函數前先聲明函數。比如下面test4.py是完全正常的代碼,函數定義順序不影響函數調用,因為在執行def語句的時候,會執行MAKE_FUNCTION指令將函數對象加入到local名字空間,而local和global此時對應的是同一個字典,所以也相當於加入了global名字空間,從而在運行函數g的時候是可以找到函數f的。另外也可以注意到,函式宣告和實現其實是分離的,聲明的位元組碼指令在模組的PyCodeObject中執行,而實現的位元組碼指令則是在函數自己的PyCodeObject中。

#test4.pydef g():                                                                                                                                                       print ‘function g‘  f() def f():  print ‘function f‘g()~      

Python源碼剖析筆記3-Python執行原理初探

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.