若是你曾經寫過或者用過 Python,你可能已經習慣了看到 Python 源代碼文件;它們的名稱以.Py 結尾。你可能還見過另外一種類型的文件是 .pyc 結尾的,它們就是 Python 「字節碼」文件。(在 Python3 的時候這個 .pyc 後綴的文件不太好找了,它在一個名爲__pycache__的子目錄下面。).pyc文件能夠防止Python每次運行時都從新解析源代碼,該文件大大節省了時間。html
Python是如何工做的python
Python 一般被描述爲一種解釋語言,在這種語言中,你的源代碼在程序運行時被翻譯成CPU指令,但這只是說對了部分。和許多解釋型語言同樣,Python 實際上將源代碼編譯爲虛擬機的一組指令,Python 解釋器就是該虛擬機的實現。其中這種中間格式稱爲「字節碼」。sass
所以,Python留下的這些.pyc文件,是爲了讓運行的速快變得 「更快」,或者是針對你的源代碼的」優化「的版本;它們是 Python 虛擬機上運行的字節碼指令。數據結構
Python 虛擬機內幕多線程
CPython使用基於堆棧的虛擬機。也就是說,它徹底圍繞堆棧數據結構(你能夠將項目「推」到結構的「頂部」,或者將項目「彈出」到「頂部」)。函數
CPython 使用三種類型的棧:優化
1.調用堆棧。這是運行中的Python程序的主要結構。對於每一個當前活動的函數調用,它都有一個項目一「幀」,堆棧的底部是程序的入口點。每次函數調用都會將新的幀推到調用堆棧上,每次函數調用返回時,它的幀都會彈出。命令行
2.在每一幀中,都有一個評估堆棧(也稱爲數據堆棧)。這個堆棧是執行 Python 函數的地方,執行Python代碼主要包括將東西推到這個堆棧上,操縱它們,而後將它們彈出。線程
3.一樣在每一幀中,都有一個塊堆棧。Python使用它來跟蹤某些類型的控制結構:循環、try /except塊,以及 with 塊都會致使條目被推送到塊堆棧上,每當退出這些結構之一時,塊堆棧就會彈出。這有助於Python知道在任何給定時刻哪些塊是活動的,例如,continue或break語句能夠影響正確的塊。翻譯
大多數 Python 字節碼指令操做的是當前調用棧幀的計算棧,雖然,還有一些指令能夠作其它的事情(好比跳轉到指定指令,或者操做塊棧)。
爲了更好地理解,假設咱們有一些調用函數的代碼,好比這個:
my_function(my_variable,2)。
Python 將轉換爲一系列字節碼指令:
一個LOAD_NAME指令,用於查找函數對象 my_function,並將其推送到計算棧的頂部。
另外一個 LOAD_NAME 指令去查找變量 my_variable,並將其推送到計算棧的頂部。
一個 LOAD_CONST 指令將一個整數 2 推送到計算棧的頂部。
一個 CALL_FUNCTION 指令。
CALL_FUNCTION 指令有2個參數,它表示 Python 須要在堆棧頂部彈出兩個位置參數; 而後函數將在它上面進行調用,而且它也同時被彈出(關鍵字參數的函數,使用指令-CALL_FUNCTION_KW-相似的操做,並配合使用第三條指令CALL_FUNCTION_EX,它適用於函數調用涉及到參數使用 * 或 ** 操做符的狀況)
一旦 Python 具有了這些,它將在調用堆棧上分配一個新的幀,填充到函數調用的本地變量,而後運行該幀內的 my_function 的字節碼。一旦運行完成,幀將從調用堆棧中彈出,在原始幀中,my_function 的返回值將被推入到計算棧的頂部。
咱們知道了這個東西了,也知道字節碼了文件了,可是如何去使用字節碼呢?ok不知道也不要緊,接下來的時間咱們全部的話題都將圍繞字節碼,在python有一個模塊能夠經過反編譯Python代碼來生成字節碼這個模塊就是今天要說的--dis模塊。
dis模塊的使用
dis模塊包括一些用於處理 Python 字節碼的函數,能夠將字節碼「反彙編」爲更便於人閱讀的形式。查看解釋器運行的字節碼還有助於優化代碼。這個模塊對於查找多線程中的競態條件也頗有用,由於能夠用它評估代碼中哪一點線程控制可能切換。參考源碼Include/opcode.h,能夠找到字節碼的正式列表。詳細能夠看官方文檔。注意不一樣版本的python生成的字節碼內容可能不同,這裏我用的Python 3.8.
訪問和理解字節碼
輸入以下內容,而後運行它:
函數 dis.dis() 將反彙編一個函數、方法、類、模塊、編譯過的 Python 代碼對象、或者字符串包含的源代碼,以及顯示出一我的類可讀的版本。dis 模塊中另外一個方便的功能是 distb()。你能夠給它傳遞一個 Python 追溯對象,或者在發生預期外狀況時調用它,而後它將在發生預期外狀況時反彙編調用棧上最頂端的函數,並顯示它的字節碼,以及插入一個指向到引起意外狀況的指令的指針。
它也能夠用於查看 Python 爲每一個函數構建的編譯後的代碼對象,由於運行一個函數將會用到這些代碼對象的屬性。這裏有一個查看 hello() 函數的示例:
代碼對象在函數中能夠以屬性 __code__ 來訪問,而且攜帶了一些重要的屬性:
許多字節碼指令--尤爲是那些推入到棧中的加載值,或者在變量和屬性中的存儲值--在這些元組中的索引做爲它們參數。
所以,如今咱們可以理解 hello() 函數中所列出的字節碼:
LOAD_GLOBAL 0:告訴 Python 經過 co_names (它是 print 函數)的索引 0 上的名字去查找它指向的全局對象,而後將它推入到計算棧。
LOAD_CONST 1:帶入 co_consts 在索引 1 上的字面值,並將它推入(索引 0 上的字面值是 None,它表示在 co_consts 中,由於 Python 函數調用有一個隱式的返回值 None,若是沒有顯式的返回表達式,就返回這個隱式的值 )。
CALL_FUNCTION 1:告訴 Python 去調用一個函數;它須要從棧中彈出一個位置參數,而後,新的棧頂將被函數調用。
「原始的」 字節碼--是非人類可讀格式的字節--也能夠在代碼對象上做爲 co_code 屬性可用。若是你有興趣嘗試手工反彙編一個函數時,你能夠從它們的十進制字節值中,使用列出 dis.opname 的方式去查看字節碼指令的名字。
基本反彙編
函數dis()能夠打印 Python 源代碼(模塊、類、方法、函數或代碼對象)的反彙編表示。能夠經過從命令行運行 dis 來反彙編 dis_simple.py 之類的模塊。
輸出按列組織,包含原始源代碼行號,代碼對象中的指令地址,操做碼名稱以及傳遞給操做碼的任何參數。
對於簡單的代碼咱們能夠經過命令行的形式執行下面的命令:
python3-mdisdis_simple.py
輸出
在這裏源代碼轉換爲4個不一樣的操做來建立和填充字典,而後將結果保存到一個局部變量。
首先解釋每一行各列參數的含義:
以第一條指令爲例:
第一列 數字(1)表示對應源代碼的行數。
第二列(可選)指示當前執行的指令(例如,當字節碼來自幀對象時)【這個例子沒有】
第三列 一個標籤,表示從以前的指令到此可能的JUMP 【這個例子沒有】
第四列 數字是字節碼中對應於字節索引的地址(這些是2的倍數,由於Python 3.6每條指令使用2個字節,而在之前的版本中可能會有所不一樣)指令LOAD_CONST在0位置。
第五列 指令自己對應的人類可讀的名字這裏是"LOAD_CONST"
第六列 Python內部用於獲取某些常量或變量,管理堆棧,跳轉到特定指令等的指令的參數(若是有的話)。
第七列 計算後的實際參數。
而後讓咱們看看這個過程:
因爲 Python 解釋器是基於棧的,因此前幾步是用LOAD_CONST將常量按正確順序放入到棧中,而後使用 BUILD_MAP 彈出要增長到字典的新鍵和值。用 STORE_NAME 將所獲得的dict對象綁定名爲my_dict.
反彙編函數
須要注意的是上面的命令行反編譯的形式,不能自動的遞歸反編譯函數,因此咱們要使用在文件中導入dis的模式進行反編譯,就像下面這樣。
運行命令
python3dis_function.py
而後獲得如下結果
要查看函數的內部,必須把函數傳遞到dis().由於這裏打印的是函數內部的東西,因此沒有顯示函數的在外層的行編號,而是從2開始的。
下面解析下每一行指令的含義:
LOAD_GLOBAL 用來加載全局變量,包括指定函數名,類名,模塊名等全局符號,這裏是len函數,LOAD_FAST 通常加載局部變量的值,也就是讀取值,用於計算或者函數調用傳參等,這裏就是傳入參數args。
通常是先指定要調用的函數,而後壓參數,最後經過 CALL_FUNCTION 調用。
STORE_FAST 保存值到局部變量。也就是把結果賦值給 STORE_FAST。
下面的print由於2個參數因此LOAD_FAST了2次,POP_TOP刪除堆棧頂部(TOS)項。LOAD_CONST加載const變量,好比數值、字符串等等,這裏由於是print因此值爲None。
最後經過RETURN_VALUE來肯定函數結尾。
要打印一個函數的總結信息咱們可使用dis的show_code的方法,它包含使用的參數和名的相關信息,show_code的參數就是這個函數對象,代碼以下:
運行以後,結果以下
能夠看到返回的內容有函數,方法,參數等信息。
反彙編類
上面咱們知道了如何反彙編一個函數的內部,一樣的咱們也能夠用相似的方法反彙編一個類。
咱們看一個例子:
運行之和獲得以下結果
從總體內容來看,結果分爲了兩部分Disassembly of __init__和Disassembly of __str__,Disassembly就是反彙編的意思。
首先分析__init__部分:
而後須要注意的一點是,方法是按照字母的順序列出的,因此在部分,先看到name再看到self,可是他們都是 LOAD_FAST。
STORE_ATTR實現self.name = name。
而後LOAD_CONST一個None和RETURN_VALUE標誌着函數結束。
接下來分析__str__部分:
LOAD_CONST將'MyObject({})'加載到棧
而後經過 LOAD_METHOD 調用字符串format方法。這個方法是Python3.7新加入的。
LOAD_FAST 也就是到了self了。
LOAD_ATTR 通常是調用某個對象的方法時。這裏就是self.name的.name操做
CALL_METHOD 是 python3.7 新增長的內容,這裏是執行方法。
RETURN_VALUE表示函數的結束。
上面字符串的拼接咱們用了format,以前我一直推薦用f-string,下面就讓咱們經過字節碼來分析,爲何f-string比format要高快。
代碼其餘代碼不變,把return改爲如下內容:
returnf'MyObject({self.name})'
再次執行,下面咱們只看__str__函數的部分。
對比發現咱們這裏沒有了調用方法的操做LOAD_METHOD,取而代之使用了用於實現fstring的FORMAT_VALUE指令。以後經過BUILD_STRING鏈接堆棧中的計數字符串並將結果字符串推入堆棧.爲何format慢呢, python中的函數調用具備至關大的開銷。 當使用str.format()時,CALL_METHOD 中花費的額外時間是致使str.format()比fstring慢得多。
使用反彙編調試
調試一個異常時,有時要查看哪一個字節碼帶來了問題。這個時候就頗有用了,要對一個錯誤周圍的代碼反彙編,有多種方法。第一種策略是在交互解釋器中使用dis()報告最後一個異常。
若是沒有向dis()傳入任何參數,那麼它會查找一個異常,並顯示致使這個異常的棧頂元素的反彙編效果。
命令行上使用
打開個人命令行執行以下操做:
行號後面的-->就是致使錯誤的操做碼,一個LOAD_NAME指令,因爲沒有定義變量i,因此沒法將與這個名關聯的值加載到棧中。
代碼中使用distb
程序還能夠打印一個活動的traceback的有關信息,將它傳遞到distb()方法。
下面的程序中有個DiviedByZero異常;可是這個公式有兩個除法,因此不清楚是哪一部分出錯,此時咱們就可使用下面的方法:
運行以後輸出
結果反映的字節碼很長咱們不用全看了,看最開始出現--> 就能夠知道錯誤的位置了。
其中SETUP_FINALLY 字節碼的含義是將try塊從try-except子句推入塊堆棧。
這裏能夠看出將LOAD_NAME 將j壓入棧以後就報錯了。因此能夠推斷出在(i/j)就出錯了。
參考資料
docs.python.org/zh-cn/3.7/l…
opensource.com/article/18/…
hackernoon.com/a-closer-lo…