中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Python執行原理的分析

發布時間:2020-08-01 09:14:36 來源:億速云 閱讀:124 作者:清晨 欄目:編程語言

這篇文章主要介紹Python執行原理的分析,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!

一、Python運行環境初始化

在看怎么執行之前,先要簡單的說明一下python的運行時環境初始化。python中有一個解釋器狀態對象PyInterpreterState用于模擬進程(后面簡稱進程對象),另外有一個線程狀態對象PyThreadState模擬線程(后面簡稱線程對象)。

python中的PyInterpreterState結構通過一個鏈表鏈接起來,用于模擬操作系統多進程。進程對象中有一個指針指向線程集合,線程對象則有一個指針指向其對應的進程對象,這樣線程和進程就關聯了起來。當然,還少不了一個當前運行線程對象_PyThreadState_Current用來維護當前運行的線程。

1、進程線程初始化

python中調用PyInitialize()函數來完成運行環境初始化。在初始化函數中,會創建進程對象interp以及線程對象并在進程對象和線程對象建立關聯,并設置當前運行線程對象為剛創建的線程對象。接下來是類型系統初始化,包括int,str,bool,list等類型初始化,這里留到后面再慢慢分析。然后,就是另外一個大頭,那就是系統模塊初始化。

進程對象interp中有一個modules變量用于維護所有的模塊對象,modules變量為字典對象,其中維護(name, module)對應關系,在python中對應著sys.modules。

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 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)>

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

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。

#示例代碼test.py
def 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.py
i = 123      
def test():
  i = 'hello world'
  print i #1

test()
print i #2

PyFunctionObject對象

PyFunctionObject是函數對象,在創建函數的指令MAKE_FUNCTION中構建。PyFunctionObject中有個func_code字段指向該函數對應的PyCodeObject對象,另外還有func_globals指向global名字空間,注意到這里并沒有使用local名字空間。調用函數時,會創建新的棧幀對象PyFrameObject來執行函數,函數調用關系通過棧幀對象PyFrameObject中的f_back字段進行關聯。

最終執行函數調用時,PyFunctionObject對象的影響已經消失,真正起作用的是PyFunctionObject的PyCodeObject對象和global名字空間,因為在創建函數棧幀時會將這兩個參數傳給PyFrameObject對象。

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虛擬機的進程,線程,棧幀對象等關系如下圖所示:

Python執行原理的分析

二、Python程序運行實例說明

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

#示例代碼test3.py
i = 1
s = 'hello world'

def test():
    k = 5
    print k
    print s

if __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_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) ##模塊本身的字節碼,下面說的整數,字符串等都是指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     #返回None


In [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.py
def g():      
  print 'function g'
  f() 

def f():
  print 'function f'

g()
~

以上是Python執行原理的分析的所有內容,感謝各位的閱讀!希望分享的內容對大家有幫助,更多相關知識,歡迎關注億速云行業資訊頻道!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

余姚市| 平湖市| 万宁市| 称多县| 勃利县| 德清县| 枞阳县| 密云县| 叙永县| 大埔区| 东兴市| 和林格尔县| 兴文县| 尖扎县| 闽侯县| 霍州市| 西盟| 醴陵市| 长阳| 遂宁市| 莱西市| 巴东县| 峨山| 南宫市| 昌江| 林甸县| 永济市| 科尔| 鹤山市| 青田县| 浑源县| 墨脱县| 安吉县| 龙口市| 广宁县| 武夷山市| 磐石市| 阿城市| 宜昌市| 太白县| 马尔康县|