国产成人精品无码青草_亚洲国产美女精品久久久久∴_欧美人与鲁交大毛片免费_国产果冻豆传媒麻婆精东

15158846557 在線咨詢 在線咨詢
15158846557 在線咨詢
所在位置: 首頁 > 營銷資訊 > 網(wǎng)站運(yùn)營 > 為何Python不像JVM那樣推出個(gè)“虛擬機(jī)版”?

為何Python不像JVM那樣推出個(gè)“虛擬機(jī)版”?

時(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ī)研究一遍。

分享一篇我看到的關(guān)于Python 虛擬機(jī)寫的最好文章 :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 解釋器把這兩步合二為一了而已。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 解釋器把這兩步合二為一了而已。

1. Python 程序執(zhí)行過程

事實(shí)上,Python 程序在執(zhí)行過程中同樣需要編譯(Compile),編譯產(chǎn)生的結(jié)果稱之為字節(jié)碼,而后由 Python 虛擬機(jī)逐行地執(zhí)行這些字節(jié)碼。所以,Python 解釋器由兩部分組成: 編譯器和虛擬機(jī)。

上圖展示了 Python 程序的執(zhí)行過程,以及 C 程序的編譯、匯編與鏈接過程,從該圖中可以非常明顯地看出 Python 與 C 程序的執(zhí)行區(qū)別。Python 如此設(shè)計(jì)的原因在于將程序的執(zhí)行與底層硬件進(jìn)一步地分離,無需擔(dān)心程序的編譯、匯編以及鏈接過程,使得 Python 程序相較于 C 程序而言更加易于移植。

這里再說一下 Python 和 Java 的區(qū)別。Java 在程序執(zhí)行時(shí)必須使用 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 文件中。

2. 編譯過程與字節(jié)碼

在 Python 的內(nèi)建函數(shù)中,定義了 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_VALUEdis 方法將返回字節(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ù)所組成。

3. 命名空間與作用域

Python 的命名空間與作用域經(jīng)常被開發(fā)者所忽略,在未深入了解 Python 虛擬機(jī)之前,我個(gè)人也認(rèn)為這些東西并不重要。但是,命名空間和變量作用域?qū)?huì)是 Python 虛擬機(jī)在執(zhí)行過程中一個(gè)非常重要的一環(huán)。

命名空間實(shí)際上是名稱到對(duì)象的一種映射,本質(zhì)上就是一個(gè)鍵-值對(duì),所以大部分的命名空間由 dict 實(shí)現(xiàn)。命名空間可以分為 3 類: 內(nèi)置命名空間,全局命名空間與局部命名空間,在作用域存在嵌套的特殊情況下,可能還會(huì)有閉包命名空間。

3.1 內(nèi)置命名空間(Build-in)

Python 語言內(nèi)置的名稱,例如內(nèi)置函數(shù)名(len, dis),內(nèi)置異常(Exception)等。

>>> import builtins>>> builtins.__dict__

3.2 全局命名空間(Global)

全局命名空間以模塊進(jìn)行劃分,每一個(gè)模塊中都包含了 dict 對(duì)象,其中保存了模塊中的變量名、類名、函數(shù)名等等。在字節(jié)碼中,全局變量的導(dǎo)入使用 LOAD_GLOBAL

3.3 局部命名空間(Local)

局部命名空間可以簡單的認(rèn)為就是函數(shù)的命名空間,例如函數(shù)參數(shù),在函數(shù)中定義的局部變量。

下面是關(guān)于局部命名空間和全局命名空間的一個(gè)非常典型的例子:

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。

首先先來看下正常的局部變量在字節(jié)碼中是如何處理的:

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)行棧。

3.4 閉包命名空間(Enclosing)

當(dāng)出現(xiàn)嵌套函數(shù)定義時(shí),或者作用域嵌套時(shí),Python 將會(huì)把內(nèi)層作用域所依賴的所有外層命名存儲(chǔ)在一個(gè)特殊的命名空間中,也就是閉包命名空間。

import logging as loggerdef foo(func): def wrapper(*args, **kwargs): logger.info(f"Execute func: {func.__name__}") func(*args, **kwargs) return wrapperfoo 閉包函數(shù)中,參數(shù) func 即屬于閉包命名空間,內(nèi)層函數(shù) wrapper 在尋找變量時(shí),若局部命名空間內(nèi)無此變量,將會(huì)于閉包命名空間中進(jìn)行查找。

如果在閉包函數(shù)中對(duì)外層函數(shù)的局部變量進(jìn)行賦值會(huì)發(fā)生什么?

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 尋找該變量的定義。

此外,閉包指函數(shù),而不是類,所以在類的嵌套中,將不會(huì)存在閉包命名空間:

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 未定義。

當(dāng)語句需要查找變量 X 時(shí),將會(huì)按照 Local -> Enclosing -> Global -> Builtin 的順序進(jìn)行查找,俗稱 LEGB 規(guī)則。

4. Python 虛擬機(jī)的執(zhí)行

4.1 執(zhí)行上下文——棧幀

在 x86-64 CPU 中包含了 16 個(gè) 64 位的通用目的寄存器,這些寄存器用于存儲(chǔ)數(shù)據(jù)或者是指針。在這 16 個(gè)通用目的寄存器中,有兩個(gè)較為特殊的寄存器: %rsp 與 %rbp。%rsp 為棧指針寄存器,表示運(yùn)行時(shí)棧的結(jié)束位置,可以簡單地理解為棧頂。%rbp 為棧幀指針,用于標(biāo)識(shí)當(dāng)前棧幀的起始位置。

在 x86 體系結(jié)構(gòu)中,函數(shù)調(diào)用是通過棧和棧幀實(shí)現(xiàn)的。當(dāng)一個(gè)函數(shù)被調(diào)用時(shí),首先做的事情就是將調(diào)用者棧幀指針入棧,以保留調(diào)用關(guān)系。其次將為調(diào)用的函數(shù)創(chuàng)建棧幀,棧幀中包含了函數(shù)的參數(shù)、創(chuàng)建的局部變量等信息。

回到 Python 虛擬機(jī)中,虛擬機(jī)在進(jìn)行函數(shù)調(diào)用時(shí),運(yùn)行方式和 x86 沒什么區(qū)別,都是由棧和棧幀所實(shí)現(xiàn)的。而棧幀則是由 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)。

下面通過一段簡單的代碼來具體看下 Python 運(yùn)行時(shí)的棧幀結(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)用鏈的起始位置。

結(jié)合前面提到的字節(jié)碼和命名空間,我們可以用一張簡圖來描述。

4.2 指令的執(zhí)行

指令執(zhí)行的源碼均位于 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)方法。

4.3 GIL 與字節(jié)碼的執(zhí)行

對(duì)于 Python 中的容器,例如 dict,并沒有實(shí)現(xiàn)像 Java 中的 ConcurrentHashMap,或者是 Golang 中的 sync.Map,這是因?yàn)?Python 中的容器(list, dict)本身就是并發(fā)安全的,但是在這些容器的源碼中并沒有發(fā)現(xiàn)定義 mutex,也就是說,Python 容器的并發(fā)安全并不是通過互斥鎖實(shí)現(xiàn)的。

實(shí)際上,Python 容器的并發(fā)安全是通過 GIL 實(shí)現(xiàn)的,也就是被廣大 Pythoner 口誅筆伐的全局解釋器鎖。某一個(gè)線程想要運(yùn)行必須要首先獲取全局鎖,如此一來,在同一時(shí)刻只能有一個(gè)線程運(yùn)行,無法充分利用多核的硬件資源。

Python 的線程調(diào)度非常類似于 CPU 的時(shí)間片實(shí)現(xiàn),只不過并不是以時(shí)間為判斷標(biāo)準(zhǔn),而是以執(zhí)行字節(jié)碼的數(shù)量作為判斷標(biāo)準(zhǔn)。當(dāng)某一個(gè)線程執(zhí)行了足夠多的字節(jié)碼條數(shù)時(shí),當(dāng)前線程將釋放全局鎖,喚醒其它線程進(jìn)行執(zhí)行。

所以,得益于 GIL 的存在,Python 容器在進(jìn)行諸如擴(kuò)容、縮容操作時(shí),完全不必?fù)?dān)心并發(fā)問題,因?yàn)橐粭l字節(jié)碼的執(zhí)行一定是原子性的。

我是 Guide哥,一個(gè)工作兩年有余,接觸編程已經(jīng)6年有余的程序員。大三開源SnailClimb/JavaGuide (如果無法訪問Github,可以訪問國內(nèi)的碼云:SnailClimb/JavaGuide),目前已經(jīng) 110k+ Star。未來幾年,希望持續(xù)完善 JavaGuide,爭取能夠幫助更多學(xué)習(xí) Java 的小伙伴!共勉!凎!

原創(chuàng)不易,歡迎點(diǎn)贊分享,歡迎關(guān)注 @JavaGuide,我會(huì)持續(xù)分享原創(chuàng)干貨~

關(guān)鍵詞:虛擬

74
73
25
news

版權(quán)所有? 億企邦 1997-2025 保留一切法律許可權(quán)利。

為了最佳展示效果,本站不支持IE9及以下版本的瀏覽器,建議您使用谷歌Chrome瀏覽器。 點(diǎn)擊下載Chrome瀏覽器
關(guān)閉