時(shí)間:2024-01-06 10:24:01 | 來源:網(wǎng)站運(yùn)營
時(shí)間:2024-01-06 10:24:01 來源:網(wǎng)站運(yùn)營
為何Python不像JVM那樣推出個(gè)“虛擬機(jī)版”?:Python 是有自帶的虛擬機(jī)的哈,只是90%學(xué)習(xí) Python 的小伙伴沒有意識(shí)到罷了!而且,你學(xué)習(xí) Python 完全沒太大必要研究 Python 虛擬機(jī),知道有這個(gè)東西就行了,不像學(xué)習(xí) Java 一定要把 Java 虛擬機(jī)研究一遍。python code.py
就可以運(yùn)行編寫的代碼,而無需使用類似于 javac
或者 gcc
進(jìn)行編譯。那么,Python 解釋器是真的一行一行讀取 Python 源代碼而后執(zhí)行嗎? 實(shí)際上,Python 在執(zhí)行程序時(shí)和 Java、C# 一樣,都是先將源碼進(jìn)行編譯生成字節(jié)碼,然后由虛擬機(jī)進(jìn)行執(zhí)行,只不過 Python 解釋器把這兩步合二為一了而已。Python 虛擬機(jī)我們常說 Python 一是門解釋型語言,只需要敲下 python code.py
就可以運(yùn)行編寫的代碼,而無需使用類似于 javac
或者 gcc
進(jìn)行編譯。那么,Python 解釋器是真的一行一行讀取 Python 源代碼而后執(zhí)行嗎? 實(shí)際上,Python 在執(zhí)行程序時(shí)和 Java、C# 一樣,都是先將源碼進(jìn)行編譯生成字節(jié)碼,然后由虛擬機(jī)進(jìn)行執(zhí)行,只不過 Python 解釋器把這兩步合二為一了而已。javac
對(duì)源代碼進(jìn)行編譯,但是并不直接編譯成機(jī)器語言,而是和 Python 一樣,編譯成字節(jié)碼,而后由 JVM 進(jìn)行執(zhí)行。從這一點(diǎn)上來看,Python 和 Java 非常類似,只不過 Python 的編譯過程由解釋器完成,用戶也可以手動(dòng)的對(duì) Python 源代碼進(jìn)行編譯,生成 .pyc
文件,節(jié)省那么一丟丟的時(shí)間。python -m compileall <dir>
通過運(yùn)行上述命令可對(duì) <dir>
目錄下所有的 Python 文件進(jìn)行編譯,編譯結(jié)果將會(huì)存放于該目錄下的 __pycache__
的 .pyc
文件中。compile
以及 exec
兩個(gè)方法,前者將源代碼編譯成為 Code Object 對(duì)象,Code Object 對(duì)象中即保存著源代碼所對(duì)應(yīng)的字節(jié)。而 exec
方法則是運(yùn)行 Python 語句或者是由 compile
方法所返回的 Code Object。exec
方法可直接運(yùn)行 Python 語句,其參數(shù)并一定需要是 Code Object。>>> snippet = "for i in range(3): print(f'Output: {i}')">>> result = compile(snippet, "", "exec")>>> result<code object <module> at 0x7f8e7e6471e0, file "", line 1>>>> exec(result)Output: 0Output: 1Output: 2
在上述代碼中定義了一個(gè)非常簡單的 Python 代碼片段,其作用就是在標(biāo)準(zhǔn)輸出中打印 0,1,2 這三個(gè)數(shù)而已。通過 compile
方法對(duì)該片段進(jìn)行編譯,得到 Code Object 對(duì)象,并將該對(duì)象交由 exec
函數(shù)執(zhí)行。下面來具體看下返回的 Code Object 中到底包含了什么。cpython/Include/code.h
中定義了 PyCodeObject
結(jié)構(gòu)體,即 Code Object 對(duì)象:/* Bytecode object */typedef struct { PyObject_HEAD /* Python定長對(duì)象頭 */ PyObject *co_code; /* 指令操作碼,即字節(jié)碼 */ PyObject *co_consts; /* 常量列表 */ PyObject *co_names; /* 名稱列表(不一定是變量,也可能是函數(shù)名稱、類名稱等) */ PyObject *co_filename; /* 源碼文件名稱 */ ... /* 省略若干字段 */} PyCodeObject;
字段 co_code
即為 Python 編譯后字節(jié)碼,其它字段在此處可暫時(shí)忽略。字節(jié)碼的格式為人類不可閱讀格式,其形式通常是這樣的:>>> result.co_codeb'x/x1ee/x00d/x00/x83/x01D/x00]/x12Z/x01e/x02d/x01e/x01/x9b/x00/x9d/x02/x83/x01/x01/x00q/nW/x00d/x02S/x00'
這個(gè)時(shí)候我們需要一個(gè)”反匯編器”來將字節(jié)碼轉(zhuǎn)換成人類可閱讀的格式,”反匯編器”打引號(hào)的原因是在 Python 中并不能稱為真正的反匯編器。>>> import dis>>> dis.dis(result.co_code) 0 SETUP_LOOP 30 (to 32) 2 LOAD_NAME 0 (0) 4 LOAD_CONST 0 (0) 6 CALL_FUNCTION 1 8 GET_ITER >> 10 FOR_ITER 18 (to 30) 12 STORE_NAME 1 (1) 14 LOAD_NAME 2 (2) 16 LOAD_CONST 1 (1) 18 LOAD_NAME 1 (1) 20 FORMAT_VALUE 0 22 BUILD_STRING 2 24 CALL_FUNCTION 1 26 POP_TOP 28 JUMP_ABSOLUTE 10 >> 30 POP_BLOCK >> 32 LOAD_CONST 2 (2) 34 RETURN_VALUE
dis
方法將返回字節(jié)碼的助記符(mnemonics),和匯編語言非常類似,從這些助記符的名稱上我們就可以大概猜出解釋器將要執(zhí)行的動(dòng)作,例如 LOAD_NAME
加載名稱,LOAD_CONST
加載常量。所以,我們完全可以將這些助記符看作是匯編指令,而指令的操作數(shù)則在助記符后面描述。例如 LOAD_NAME
操作,其操作數(shù)的下標(biāo)為 0,而在源代碼中使用過的名稱保存在 co_names
字段中,所以 LOAD_NAME 0
即表示加載 result.co_names[0]
:>>> result.co_names[0]'range'
又比如 LOAD_CONST
操作,其操作數(shù)的下標(biāo)也為 0,只不過這次操作數(shù)不再保存在 co_names
,而是 co_consts
中,所以 LOAD_CONST 0
則表示加載 result.co_consts[0]
:>>> result.co_consts[0]3
由于 Code Object 對(duì)象保存了常量、變量、名稱等一系列的上下文內(nèi)容,所以可以直接對(duì)該對(duì)象進(jìn)行反匯編操作:>>> dis.dis(result) 1 0 SETUP_LOOP 30 (to 32) 2 LOAD_NAME 0 (range) 4 LOAD_CONST 0 (3) ...
現(xiàn)在,我們可以對(duì) Python 字節(jié)碼做一下小結(jié)了。Python 在編譯某段源碼時(shí),并不會(huì)直接返回字節(jié)碼,而是返回一個(gè) Code Object 對(duì)象,字節(jié)碼則保存在該對(duì)象的 co_code
字段中。由于字節(jié)碼是一個(gè)二進(jìn)制字節(jié)序列,無法直接進(jìn)行閱讀,所以需要通過”反匯編器”(dis
模塊)將字節(jié)碼轉(zhuǎn)換成人類可讀的助記符。助記符的形式和匯編語言非常類似,均由操作指令+操作數(shù)所組成。dict
實(shí)現(xiàn)。命名空間可以分為 3 類: 內(nèi)置命名空間,全局命名空間與局部命名空間,在作用域存在嵌套的特殊情況下,可能還會(huì)有閉包命名空間。len
, dis
),內(nèi)置異常(Exception
)等。>>> import builtins>>> builtins.__dict__
dict
對(duì)象,其中保存了模塊中的變量名、類名、函數(shù)名等等。在字節(jié)碼中,全局變量的導(dǎo)入使用 LOAD_GLOBAL
。number = 10def foo():number += 10print(number)if **name** == "**main**":foo()UnboundLocalError: local variable 'number' referenced before assignment
在運(yùn)行上述代碼時(shí)將會(huì)拋出 UnboundLocalError
異常,這簡直莫名其妙,在其它語言中,上述代碼都能夠正常運(yùn)行,以 C 語言為例:#include <stdio.h>int number = 10;int main() { number += 10; printf("%d/n", number); // 正常運(yùn)行并打印結(jié)果: 20}
但是在 Python 中卻拋出了異常,這又是為什么? 官方在 Python FAQ 給出了相關(guān)解釋,原文如下:In Python, variables that are only referenced inside a function are implicitly global. If a variable is assigned a value anywhere within the function’s body, it’s assumed to be a local unless explicitly declared as global.簡單來說,當(dāng)我們?cè)诤瘮?shù)中引用一個(gè)變量時(shí),Python 將隱式地默認(rèn)該變量為全局變量。但是,一旦變量在沒有
global
關(guān)鍵字修飾的情況下進(jìn)行了賦值操作,Python 會(huì)將其作為局部變量處理。number += 10
進(jìn)行了賦值動(dòng)作,此時(shí) number
為局部變量,該函數(shù)中又沒有聲明該局部變量,故而拋出異常。Python 這么做的目的就是為了防止開發(fā)者者在某些函數(shù)中修改了全局變量而又不自知,通過顯式地添加 global
關(guān)鍵字來確保開發(fā)者知道自己在做什么。這正如 Python 之禪所述的,Explicit is better than implicit。import disdef foo(): number = 10 print(number)if __name__ == "__main__": print(dis.dis(foo))0 LOAD_CONST 1 (10)2 STORE_FAST 0 (number)4 LOAD_GLOBAL 0 (print)6 LOAD_FAST 0 (number)
STORE_FAST
將當(dāng)前變量壓入到函數(shù)運(yùn)行棧中,而 LOAD_FAST
則從函數(shù)運(yùn)行棧取出該變量。LOAD_FAST
之前必須存在 STORE_FAST
,否則在運(yùn)行時(shí)將會(huì)拋出異常。對(duì)于最初的例子而言,在未添加 global
關(guān)鍵字的情況下,語句 number += 10
將會(huì)直接執(zhí)行 LOAD_FAST
指令,而此時(shí)當(dāng)前變量并未壓入至當(dāng)前函數(shù)運(yùn)行棧。import logging as loggerdef foo(func): def wrapper(*args, **kwargs): logger.info(f"Execute func: {func.__name__}") func(*args, **kwargs) return wrapper
在 foo
閉包函數(shù)中,參數(shù) func
即屬于閉包命名空間,內(nèi)層函數(shù) wrapper
在尋找變量時(shí),若局部命名空間內(nèi)無此變量,將會(huì)于閉包命名空間中進(jìn)行查找。def foo(): number = 10 def bar(): number += 10 return bar
正如同在局部命名空間中提到的一樣,當(dāng)一個(gè)變量在函數(shù)中被賦值時(shí),Python 默認(rèn)將其作為全局變量,既不是局部變量,也不是這里提到的閉包空間變量。所以,當(dāng)我們?cè)趯?shí)際運(yùn)行 bar
方法時(shí),同樣會(huì)得到 UnboundLocalError
異常。在這里如果想要使用 foo
函數(shù)中的 number
變量的話,需要使用 nonlocal
關(guān)鍵字進(jìn)行修飾,讓 Python 去 bar
函數(shù)的最近外層,也就是 foo
尋找該變量的定義。class Reader(object): BUFFER_SIZE = 4096 class ReaderInternal(object): def __init__(self): self._buffer_size = BUFFER_SIZE * 2 # ...if __name__ == "__main__": Reader.ReaderInternal()
在執(zhí)行 Reader.ReaderInternal()
語句時(shí),將會(huì)拋出 NameError
的異常,表示 BUFFER_SIZE
未定義。X
時(shí),將會(huì)按照 Local -> Enclosing -> Global -> Builtin 的順序進(jìn)行查找,俗稱 LEGB 規(guī)則。PyFrameObject
表示,于源碼 cpython/Include/frameobject.h
中定義。typedef struct _frame { PyObject_VAR_HEAD /* Python固定長度對(duì)象頭 */ struct _frame *f_back; /* 指向上一個(gè)棧幀的指針 */ PyCodeObject *f_code; /* Code Object代碼對(duì)象,其中包含了字節(jié)碼 */ PyObject *f_builtins; /* 內(nèi)建命名空間字典(PyDictObject) */ PyObject *f_globals; /* 全局命名空間字典(PyDictObject) */ PyObject *f_locals; /* 局部命名空間表(通常是數(shù)組) */ int f_lasti; /* 上一條指令編號(hào) */ ...} PyFrameObject;
可以看到,在一個(gè)棧幀中包含了 Code Object 代碼對(duì)象,三個(gè)命名空間表,上一個(gè)棧幀指針等信息。可以說,PyFrameObject
對(duì)象包含了 Python 虛擬機(jī)執(zhí)行所需的全部上下文。在 Python 源碼層面,可以通過 sys
模塊中的 _getframe
方法來獲取當(dāng)前函數(shù)運(yùn)行時(shí)的棧幀,方法將返回 FrameType
類型,其實(shí)就是 PyFrameObject
簡化后的 Python 結(jié)構(gòu)。import sysdef first(): middle()def middle(): finish()def finish(): print_frame()def print_frame(): current_frame = sys._getframe() while current_frame: print(f"func name: {current_frame.f_code.co_name}") print("*" * 20) current_frame = current_frame.f_backif __name__ == "__main__": first()func name: print_frame****************************************func name: finish****************************************func name: middle****************************************func name: first****************************************func name: <module>****************************************
在 Python 開始執(zhí)行該程序時(shí),首先創(chuàng)建一個(gè)用于執(zhí)行模塊代碼對(duì)象的棧幀對(duì)象,也就是 module
。隨著一個(gè)一個(gè)的函數(shù)調(diào)用,不同的棧幀對(duì)象將會(huì)被創(chuàng)建,并且壓入至運(yùn)行棧中,而連接這些棧幀對(duì)象的紐帶就是 f_back
指針。當(dāng)棧頂?shù)暮瘮?shù)執(zhí)行完畢開始返回時(shí),將沿著 f_back
指針方向一直到當(dāng)前調(diào)用鏈的起始位置。cpython/Python/ceval.c
中,入口函數(shù)有兩個(gè),一個(gè)是 PyEval_EvalCode
,另一個(gè)則是 PyEval_EvalCodeEx
,最終的實(shí)際調(diào)用函數(shù)為 _PyEval_EvalCodeWithName
,所以我們只需要關(guān)注該函數(shù)即可。_PyEval_EvalCodeWithName
函數(shù)的主要作用為進(jìn)行函數(shù)調(diào)用的例常檢查,例如校驗(yàn)函數(shù)參數(shù)的個(gè)數(shù)、類型,校驗(yàn)關(guān)鍵字參數(shù)等。除此之外,該函數(shù)將會(huì)初始化棧幀對(duì)象并將其交給 PyEval_EvalFrame
函數(shù)進(jìn)行處理,最終由 _PyEval_EvalFrameDefault
函數(shù)真正的運(yùn)行指令。_PyEval_EvalFrameDefault
函數(shù)定義超過了 3K 行,絕大部分的邏輯其實(shí)都是 switch-case
: 根據(jù)指令類型執(zhí)行相應(yīng)的邏輯。for (;;) { switch (opcode) { case TARGET(LOAD_CONST): { /* 加載常量 */ ... } case TARGET(ROT_TWO): { /* 交換兩個(gè)變量 */ ... } case TARGET(FORMAT_VALUE):{ /* 格式化字符串 */ ... }
可以看到 TARGET()
調(diào)用中的參數(shù)其實(shí)就是 dis
方法返回的助記符,當(dāng)我們?cè)诜治鲋浄木唧w實(shí)現(xiàn)邏輯時(shí),可以在該文件中找到對(duì)應(yīng)的 C 實(shí)現(xiàn)方法。ConcurrentHashMap
,或者是 Golang 中的 sync.Map
,這是因?yàn)?Python 中的容器(list, dict)本身就是并發(fā)安全的,但是在這些容器的源碼中并沒有發(fā)現(xiàn)定義 mutex
,也就是說,Python 容器的并發(fā)安全并不是通過互斥鎖實(shí)現(xiàn)的。關(guān)鍵詞:虛擬
客戶&案例
營銷資訊
關(guān)于我們
微信公眾號(hào)
版權(quán)所有? 億企邦 1997-2025 保留一切法律許可權(quán)利。