Python字节码介绍
了解 Python 字節(jié)碼是什么,Python 如何使用它來(lái)執(zhí)行你的代碼,以及知道它是如何幫到你的。
如果你曾經(jīng)編寫過(guò) Python,或者只是使用過(guò) Python,你或許經(jīng)常會(huì)看到 Python 源代碼文件——它們的名字以?.py?結(jié)尾。你可能還看到過(guò)其它類型的文件,比如以?.pyc?結(jié)尾的文件,或許你可能聽(tīng)說(shuō)過(guò)它們就是 Python 的 “字節(jié)碼bytecode” 文件。(在 Python 3 上這些可能不容易看到 —— 因?yàn)樗鼈兣c你的?.py?文件不在同一個(gè)目錄下,它們?cè)谝粋€(gè)叫?__pycache__?的子目錄中)或者你也聽(tīng)說(shuō)過(guò),這是節(jié)省時(shí)間的一種方法,它可以避免每次運(yùn)行 Python 時(shí)去重新解析源代碼。
但是,除了 “噢,原來(lái)這就是 Python 字節(jié)碼” 之外,你還知道這些文件能做什么嗎?以及 Python 是如何使用它們的?
如果你不知道,那你走運(yùn)了!今天我將帶你了解 Python 的字節(jié)碼是什么,Python 如何使用它去運(yùn)行你的代碼,以及知道它是如何幫助你的。
Python 如何工作
Python 經(jīng)常被介紹為它是一個(gè)解釋型語(yǔ)言 —— 其中一個(gè)原因是在程序運(yùn)行時(shí),你的源代碼被轉(zhuǎn)換成 CPU 的原生指令 —— 但這樣的看法只是部分正確。Python 與大多數(shù)解釋型語(yǔ)言一樣,確實(shí)是將源代碼編譯為一組虛擬機(jī)指令,并且 Python 解釋器是針對(duì)相應(yīng)的虛擬機(jī)實(shí)現(xiàn)的。這種中間格式被稱為 “字節(jié)碼”。
因此,這些?.pyc?文件是 Python 悄悄留下的,是為了讓它們運(yùn)行的 “更快”,或者是針對(duì)你的源代碼的 “優(yōu)化” 版本;它們是你的程序在 Python 虛擬機(jī)上運(yùn)行的字節(jié)碼指令。
我們來(lái)看一個(gè)示例。這里是用 Python 寫的經(jīng)典程序 “Hello, World!”:
?
下面是轉(zhuǎn)換后的字節(jié)碼(轉(zhuǎn)換為人類可讀的格式):
?
如果你輸入那個(gè)?hello()?函數(shù),然后使用?CPython?解釋器去運(yùn)行它,那么上述列出的內(nèi)容就是 Python 所運(yùn)行的。它看起來(lái)可能有點(diǎn)奇怪,因此,我們來(lái)深入了解一下它都做了些什么。
Python 虛擬機(jī)內(nèi)幕
CPython 使用一個(gè)基于棧的虛擬機(jī)。也就是說(shuō),它完全面向棧數(shù)據(jù)結(jié)構(gòu)的(你可以 “推入” 一個(gè)東西到棧 “頂”,或者,從棧 “頂” 上 “彈出” 一個(gè)東西來(lái))。
CPython 使用三種類型的棧:
調(diào)用棧call stack。這是運(yùn)行 Python 程序的主要結(jié)構(gòu)。它為每個(gè)當(dāng)前活動(dòng)的函數(shù)調(diào)用使用了一個(gè)東西 —— “幀frame”,棧底是程序的入口點(diǎn)。每個(gè)函數(shù)調(diào)用推送一個(gè)新的幀到調(diào)用棧,每當(dāng)函數(shù)調(diào)用返回后,這個(gè)幀被銷毀。
在每個(gè)幀中,有一個(gè)計(jì)算棧evaluation stack (也稱為數(shù)據(jù)棧data stack)。這個(gè)棧就是 Python 函數(shù)運(yùn)行的地方,運(yùn)行的 Python 代碼大多數(shù)是由推入到這個(gè)棧中的東西組成的,操作它們,然后在返回后銷毀它們。
在每個(gè)幀中,還有一個(gè)塊棧block stack。它被 Python 用于去跟蹤某些類型的控制結(jié)構(gòu):循環(huán)、try?/?except?塊、以及?with?塊,全部推入到塊棧中,當(dāng)你退出這些控制結(jié)構(gòu)時(shí),塊棧被銷毀。這將幫助 Python 了解任意給定時(shí)刻哪個(gè)塊是活動(dòng)的,比如,一個(gè)?continue?或者?break?語(yǔ)句可能影響正確的塊。
大多數(shù) Python 字節(jié)碼指令操作的是當(dāng)前調(diào)用棧幀的計(jì)算棧,雖然,還有一些指令可以做其它的事情(比如跳轉(zhuǎn)到指定指令,或者操作塊棧)。
為了更好地理解,假設(shè)我們有一些調(diào)用函數(shù)的代碼,比如這個(gè):my_function(my_variable, 2)。Python 將轉(zhuǎn)換為一系列字節(jié)碼指令:
一個(gè)?LOAD_NAME?指令去查找函數(shù)對(duì)象?my_function,然后將它推入到計(jì)算棧的頂部
另一個(gè)?LOAD_NAME?指令去查找變量?my_variable,然后將它推入到計(jì)算棧的頂部
一個(gè)?LOAD_CONST?指令去推入一個(gè)實(shí)整數(shù)值?2?到計(jì)算棧的頂部
一個(gè)?CALL_FUNCTION?指令
這個(gè)?CALL_FUNCTION?指令將有 2 個(gè)參數(shù),它表示那個(gè) Python 需要從棧頂彈出兩個(gè)位置參數(shù);然后函數(shù)將在它上面進(jìn)行調(diào)用,并且它也同時(shí)被彈出(對(duì)于函數(shù)涉及的關(guān)鍵字參數(shù),它使用另一個(gè)不同的指令 ——?CALL_FUNCTION_KW,但使用的操作原則類似,以及第三個(gè)指令 ——?CALL_FUNCTION_EX,它適用于函數(shù)調(diào)用涉及到參數(shù)使用?*?或?**?操作符的情況)。一旦 Python 擁有了這些之后,它將在調(diào)用棧上分配一個(gè)新幀,填充到函數(shù)調(diào)用的本地變量上,然后,運(yùn)行那個(gè)幀內(nèi)的?my_function?字節(jié)碼。運(yùn)行完成后,這個(gè)幀將被調(diào)用棧銷毀,而在最初的幀內(nèi),my_function?的返回值將被推入到計(jì)算棧的頂部。
訪問(wèn)和理解 Python 字節(jié)碼
如果你想玩轉(zhuǎn)字節(jié)碼,那么,Python 標(biāo)準(zhǔn)庫(kù)中的?dis?模塊將對(duì)你有非常大的幫助;dis?模塊為 Python 字節(jié)碼提供了一個(gè) “反匯編”,它可以讓你更容易地得到一個(gè)人類可讀的版本,以及查找各種字節(jié)碼指令。dis?模塊的文檔?可以讓你遍歷它的內(nèi)容,并且提供一個(gè)字節(jié)碼指令能夠做什么和有什么樣的參數(shù)的完整清單。
例如,獲取上面的?hello()?函數(shù)的列表,可以在一個(gè) Python 解析器中輸入如下內(nèi)容,然后運(yùn)行它:
?
函數(shù)?dis.dis()?將反匯編一個(gè)函數(shù)、方法、類、模塊、編譯過(guò)的 Python 代碼對(duì)象、或者字符串包含的源代碼,以及顯示出一個(gè)人類可讀的版本。dis?模塊中另一個(gè)方便的功能是?distb()。你可以給它傳遞一個(gè) Python 追溯對(duì)象,或者在發(fā)生預(yù)期外情況時(shí)調(diào)用它,然后它將在發(fā)生預(yù)期外情況時(shí)反匯編調(diào)用棧上最頂端的函數(shù),并顯示它的字節(jié)碼,以及插入一個(gè)指向到引發(fā)意外情況的指令的指針。
它也可以用于查看 Python 為每個(gè)函數(shù)構(gòu)建的編譯后的代碼對(duì)象,因?yàn)檫\(yùn)行一個(gè)函數(shù)將會(huì)用到這些代碼對(duì)象的屬性。這里有一個(gè)查看?hello()?函數(shù)的示例:
?
代碼對(duì)象在函數(shù)中可以以屬性?__code__?來(lái)訪問(wèn),并且攜帶了一些重要的屬性:
co_consts?是存在于函數(shù)體內(nèi)的任意實(shí)數(shù)的元組
co_varnames?是函數(shù)體內(nèi)使用的包含任意本地變量名字的元組
co_names?是在函數(shù)體內(nèi)引用的任意非本地名字的元組
許多字節(jié)碼指令 —— 尤其是那些推入到棧中的加載值,或者在變量和屬性中的存儲(chǔ)值 —— 在這些元組中的索引作為它們參數(shù)。
因此,現(xiàn)在我們能夠理解?hello()?函數(shù)中所列出的字節(jié)碼:
LOAD_GLOBAL 0:告訴 Python 通過(guò)?co_names?(它是?print?函數(shù))的索引 0 上的名字去查找它指向的全局對(duì)象,然后將它推入到計(jì)算棧
LOAD_CONST 1:帶入?co_consts?在索引 1 上的字面值,并將它推入(索引 0 上的字面值是?None,它表示在?co_consts?中,因?yàn)?Python 函數(shù)調(diào)用有一個(gè)隱式的返回值?None,如果沒(méi)有顯式的返回表達(dá)式,就返回這個(gè)隱式的值 )。
CALL_FUNCTION 1:告訴 Python 去調(diào)用一個(gè)函數(shù);它需要從棧中彈出一個(gè)位置參數(shù),然后,新的棧頂將被函數(shù)調(diào)用。
“原始的” 字節(jié)碼 —— 是非人類可讀格式的字節(jié) —— 也可以在代碼對(duì)象上作為?co_code屬性可用。如果你有興趣嘗試手工反匯編一個(gè)函數(shù)時(shí),你可以從它們的十進(jìn)制字節(jié)值中,使用列出?dis.opname?的方式去查看字節(jié)碼指令的名字。
字節(jié)碼的用處
現(xiàn)在,你已經(jīng)了解的足夠多了,你可能會(huì)想 “OK,我認(rèn)為它很酷,但是知道這些有什么實(shí)際價(jià)值呢?”由于對(duì)它很好奇,我們?nèi)チ私馑?#xff0c;但是除了好奇之外,Python 字節(jié)碼在幾個(gè)方面還是非常有用的。
首先,理解 Python 的運(yùn)行模型可以幫你更好地理解你的代碼。人們都開(kāi)玩笑說(shuō),C 是一種 “可移植匯編器”,你可以很好地猜測(cè)出一段 C 代碼轉(zhuǎn)換成什么樣的機(jī)器指令。理解 Python 字節(jié)碼之后,你在使用 Python 時(shí)也具備同樣的能力 —— 如果你能預(yù)料到你的 Python 源代碼將被轉(zhuǎn)換成什么樣的字節(jié)碼,那么你可以知道如何更好地寫和優(yōu)化 Python 源代碼。
第二,理解字節(jié)碼可以幫你更好地回答有關(guān) Python 的問(wèn)題。比如,我經(jīng)??吹揭恍?Python 新手困惑為什么某些結(jié)構(gòu)比其它結(jié)構(gòu)運(yùn)行的更快(比如,為什么?{}?比?dict()?快)。知道如何去訪問(wèn)和閱讀 Python 字節(jié)碼將讓你很容易回答這樣的問(wèn)題(嘗試對(duì)比一下:?dis.dis("{}")?與?dis.dis("dict()")?就會(huì)明白)。
最后,理解字節(jié)碼和 Python 如何運(yùn)行它,為 Python 程序員不經(jīng)常使用的一種特定的編程方式提供了有用的視角:面向棧的編程。如果你以前從來(lái)沒(méi)有使用過(guò)像 FORTH 或 Fator 這樣的面向棧的編程語(yǔ)言,它們可能有些古老,但是,如果你不熟悉這種方法,學(xué)習(xí)有關(guān) Python 字節(jié)碼的知識(shí),以及理解面向棧的編程模型是如何工作的,將有助你開(kāi)拓你的編程視野。
加vx;tanzzhouyiwan 免費(fèi)領(lǐng)取Python學(xué)習(xí)資料一套哦!
?
---------------------
作者:sxyyu1
來(lái)源:CSDN
原文:https://blog.csdn.net/sxyyu1/article/details/82841410
版權(quán)聲明:本文為博主原創(chuàng)文章,轉(zhuǎn)載請(qǐng)附上博文鏈接!
總結(jié)
以上是生活随笔為你收集整理的Python字节码介绍的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 关于python 中的__future_
- 下一篇: 矩阵的阶