python字節碼

Python如何工作

Python經常被描述為一種解釋型語言 - 當程序運行時,你的源代碼被翻譯成本地CPU指令 - 但這只是部分正確的。與許多解釋型語言一樣,Python實際上將源代碼編譯為虛擬機的一組指令,Python解釋器是該虛擬機的一個實現。這種中間格式被稱為“字節碼”。

因此,.pycPython留下的文件不僅僅是源代碼的一些“更快”或“優化”版本; 它們是在您的程序運行時將由Python的虛擬機執行的字節碼指令。

我們來看一個例子。這是一個經典的“你好,世界!” 用Python編寫:

[python] view plain copy

  1. def hello()
  2. print("Hello, World!")

這裡是它變成的字節碼(翻譯成人類可讀的形式):

[python] view plain copy

  1. 0 LOAD_GLOBAL 0 (print)
  2. 2 LOAD_CONST 1 ('Hello, World!')
  3. 4 CALL_FUNCTION 1

如果您鍵入該hello()函數並使用CPython解釋器運行它,則上面的列表是Python將執行的內容。不過,它可能看起來有點奇怪,所以讓我們深入瞭解發生了什麼。

Python虛擬機中

CPython使用基於堆棧的虛擬機。也就是說,它完全圍繞著堆棧數據結構(您可以將一個項目“推”到該結構的“頂部”,或者從“頂部”“彈出”一個項目)。

CPython使用三種類型的堆棧:

1、該調用堆棧。這是一個正在運行的Python程序的主要結構。它有一個項目 - 一個“框架” - 用於每個當前活動的函數調用,堆棧的底部是程序的入口點。每個函數調用都會將新框架推入調用堆棧,並且每次函數調用返回時,其框架都會彈出。

在每一幀中,都有一個評估堆棧(也稱為數據堆棧)。這個棧是執行Python函數的地方,執行Python代碼主要包括把東西推到這個棧上,操作它們,並將它們彈回去。

2、同樣在每一幀中,都有一個塊堆棧。Python使用它來跟蹤某些類型的控制結構:循環,try/ except塊和with塊將所有條目都壓入塊堆棧,並且塊堆棧在您退出其中一個結構時彈出。這有助於Python知道哪些塊在任何特定時刻處於活動狀態,例如,a continue或break語句可能會影響正確的塊。

3、儘管有一些指令可以執行其他操作(如跳轉到特定指令或操作塊堆棧),但Python的大部分字節碼指令操作當前調用堆棧幀的評估堆棧。

為了感受這一點,假設我們有一些調用函數的代碼,如下所示:my_function(my_variable, 2)。Python會將其轉換為四個字節碼指令序列:

1、一條 LOAD_NAME那查找函數對象指令my_function並將其推到評價堆棧的頂部

2、另一條LOAD_NAME查找變量my_variable並將其推到評估堆棧頂部的指令

3、一條 LOAD_CONST指令推字面整數值2對評價堆棧的頂部

4、一條CALL_FUNCTION指令

該CALL_FUNCTION指令的參數為2,表示Python需要從堆棧頂部彈出兩個位置參數; 那麼調用的函數將處於最前面,並且它也可以被彈出(對於涉及關鍵字參數的函數,使用了不同的指令CALL_FUNCTION_KW- - 但具有類似的操作原理,並且第三條指令CALL_FUNCTION_EX用於函數涉及與*或**運營商拆開包裝的調用)。一旦Python擁有了所有這些功能,它將在調用堆棧上分配一個新框架,為函數調用填充局部變量,並執行該my_function框架內的字節碼。一旦完成,框架將從調用堆棧中彈出,並在原始框架中返回值my_function 將被推到評估堆棧頂部。

訪問和理解Python字節碼

如果您想要使用這個,那麼Python標準庫中的dis模塊是一個巨大的幫助;dis模塊為Python字節碼提供了一個“反彙編器”,使其易於獲得人類可讀的版本,並查找各種字節碼指令。dis模塊的文檔會檢查它的內容。提供一個完整的字節碼指令列表,以及它們所做的和它們的參數。

例如,要獲取上述hello()函數的字節碼列表,我將它鍵入Python解釋器,然後運行:

[python] view plain copy

  1. import dis
  2. dis.dis(hello)

該函數dis.dis()將反彙編函數,方法,類,模塊,編譯的Python代碼對象或包含源代碼的字符串文本,並打印出可讀的版本。dis模塊中另一個方便的功能是distb()。您可以將它傳遞給Python traceback對象,或者在引發異常之後調用它,並且它會在異常時分解調用堆棧中的最頂層函數,打印其字節碼,並向引發該指令的指令插入一個指針例外。

查看Python為每個函數編譯的編譯代碼對象也很有用,因為執行一個函數會利用這些代碼對象的屬性。以下是查看該hello()功能的示例:

[python] view plain copy

  1. >>> hello.__code__
  2. ", line 1>
  3. >>> hello.__code__.co_consts
  4. (None, 'Hello, World!')
  5. >>> hello.__code__.co_varnames
  6. ()
  7. >>> hello.__code__.co_names
  8. ('print',)

代碼對象可以作為__code__函數的屬性訪問,並具有一些重要的屬性:

co_consts 是函數體中出現的任何文字的元組

co_varnames 是一個包含函數體中使用的任何局部變量名稱的元組

co_names 是函數體中引用的任何非本地名稱的元組

許多字節碼指令 - 尤其是那些加載值被壓入堆棧或將值存儲在變量和屬性中的指令 - 使用這些元組中的索引作為它們的參數。

所以現在我們可以瞭解該hello()函數的字節碼列表:

LOAD_GLOBAL 0:告訴Python在co_names(它是print函數)的索引0處查找由名稱引用的全局對象並將其推送到評估堆棧

CALL_FUNCTION 1:告訴Python調用一個函數; 它需要從堆棧中彈出一個位置參數,然後新的堆棧頂部將是要調用的函數。

"raw"字節碼 - 作為非人類可讀的字節 - 在代碼對象上也可用作屬性co_code。dis.opname如果您想嘗試手動反彙編函數,則可以使用列表從其小數字節值中查找字節碼指令的名稱。

使用字節碼

現在你已經讀了這麼多,你可能會想:“好吧,我猜這很酷,但是知道這個的實際價值是什麼?” 為了好奇而拋開好奇心,理解Python字節碼在幾個方面是有用的。

首先,理解Python的執行模型可以幫助你推理你的代碼。人們喜歡開玩笑說C是一種“便攜式彙編程序”,你可以很好地猜測C語言源代碼中哪些機器指令會變成什麼樣。理解字節碼將為您提供與Python相同的功能 - 如果您可以預測Python源代碼轉換為什麼字節碼,則可以更好地決定如何編寫和優化它。

其次,瞭解字節碼是回答有關Python的問題的有用方法。例如,我經常看到較新的Python程序員想知道為什麼某些結構比其他結構更快(比如為什麼{}要快dict())。知道如何訪問和讀取Python字節碼可以讓你找出答案(嘗試:dis.dis("{}")與dis.dis("dict()"))。

最後,理解字節碼以及Python如何執行它,對於Python程序員不經常參與的特定類型的編程提供了一個有用的視角:面向堆棧的編程。如果您曾經使用過像FORTH或Factor這樣的面向堆棧的語言,這可能是一箇舊消息,但如果您不熟悉這種方法,那麼瞭解Python字節碼並瞭解其面向堆棧的編程模型如何工作就是一個整潔擴大你的編程知識的方法。

python字節碼


分享到:


相關文章: