Python編譯和運(yùn)行

Python雖然是一門(mén)解釋型語(yǔ)言,但Python程序執(zhí)行時(shí),也需要將源碼進(jìn)行編譯生成字節(jié)碼,然后由Python虛擬機(jī)進(jìn)行執(zhí)行,因此Python解釋器實(shí)際是由兩部分組成:編譯器虛擬機(jī)
Python程序執(zhí)行過(guò)程和Java類(lèi)似,都是先將代碼編譯成字節(jié)碼,然后由虛擬機(jī)執(zhí)行:

Python執(zhí)行過(guò)程

編譯Python程序

在Java中,使用javac命令調(diào)用編譯器(JavaCompile)將.java文件編譯成字節(jié)碼并輸出.class字節(jié)碼文件,然后使用java命令通過(guò)JVM執(zhí)行編譯后的字節(jié)碼文件;

在Python中,由于編譯器和虛擬機(jī)合二為一,所以沒(méi)有區(qū)分編譯器和虛擬機(jī)運(yùn)行的命令,使用python命令調(diào)用python解釋器,默認(rèn)就會(huì)將.py文件編譯并運(yùn)行。

py_compile.compile函數(shù)

但既然需要編譯后運(yùn)行,那當(dāng)然會(huì)有編譯功能模塊,python可以調(diào)用compileall.pypy_compile.py模塊來(lái)編譯.py文件并生成.pyc字節(jié)碼文件

compileall.py模塊

compileall.py模塊有compile_dir、compile_file兩個(gè)個(gè)函數(shù),用于對(duì)指定目錄或文件進(jìn)行編譯。通過(guò)Python命令參數(shù)調(diào)用時(shí),compile_path函數(shù)會(huì)通過(guò)傳入路徑參數(shù)判斷編譯目標(biāo)是目錄還是文件
在桌面編寫(xiě)demo.py程序,使用python -m compileall demo.py編譯:

compileall編譯py文件

編譯完成后,在demo.py同級(jí)目錄下生成了一個(gè)__pycache__目錄,目錄下有一個(gè)demo.cpython-37.pyc文件(python使用Python3.7版本):
生成的pyc文件

更多關(guān)于compileall的內(nèi)容詳見(jiàn):https://docs.python.org/zh-cn/3.7/library/compileall.html

py_compile.py模塊

compileall內(nèi)部最終調(diào)用的是py_compile.py模塊的compile函數(shù),compile函數(shù)是最終完成代碼編譯并輸出字節(jié)碼文件的函數(shù),因此也可以使用python -m py_compile demo.py對(duì)代碼進(jìn)行編譯:

py_compile編譯py文件

更多關(guān)于py_compile的內(nèi)容詳見(jiàn):https://docs.python.org/zh-cn/3.7/library/py_compile.html

內(nèi)建函數(shù)compile

python還有一個(gè)內(nèi)建函數(shù)compile,用于將源碼編譯成代碼或 AST 對(duì)象。代碼對(duì)象可以被 exec() 或 eval() 執(zhí)行。源碼可以是常規(guī)的字符串、字節(jié)字符串,或者 AST 對(duì)象。bltinmodule.c中compile函數(shù)的定義:

/*  
source: object  
filename: object(converter="PyUnicode_FSDecoder")  
mode: str  
flags: int = 0  
dont_inherit: bool(accept={int}) = False  
optimize: int = -1  
*  
_feature_version as feature_version: int = -1

將源代碼編譯成可由exec()或eval()執(zhí)行的代碼對(duì)象

source: 源代碼可以是一個(gè)Python模塊、語(yǔ)句或表達(dá)式  
filename: 文件名將用于運(yùn)行時(shí)錯(cuò)誤消息  
mode: 編譯模塊的模式必須是'exec',編譯單個(gè)(交互式)語(yǔ)句的模式必須是'single',  
編譯表達(dá)式的模式必須是'eval'  
...  
*/
static PyObject *  
builtin_compile_impl(PyObject _module, PyObject_ source, PyObject *filename,  
                     const char *mode, int flags, int dont_inherit,  
                     int optimize, int feature_version)  
{  
    ...C語(yǔ)言源碼實(shí)現(xiàn)位于cpython/Python/bltinmodule.c,有興趣的可以去github查看完整源碼  
}

compile函數(shù)使用方法:

  • compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1)

下面來(lái)看看compile的用法:

>>> source = 'for i in range(3): print(i)'  
>>> result = compile(source, '', 'exec')  
>>> result  
<code object at 0x0000028788A27660, file "", line 1>

可以看到,編譯后返回了一個(gè)CodeObject對(duì)象,這個(gè)對(duì)象是可以被exec函數(shù)執(zhí)行的:

>>> exec(result)
0  
1  
2

編譯運(yùn)行過(guò)程

從上述內(nèi)容可知,py_compile.py模塊中的compile函數(shù)由python實(shí)現(xiàn),只能對(duì).py文件進(jìn)行編譯,編譯完成后返回字節(jié)碼文件路徑;
而內(nèi)建函數(shù)compile由C語(yǔ)言實(shí)現(xiàn),可以對(duì)代碼語(yǔ)句進(jìn)行編譯,編譯完成后返回一個(gè)CdoeObject對(duì)象。
那這個(gè)CodeObject對(duì)象是什么呢?
在上述的compile源碼中,可以看到函數(shù)返回的是PyObject,這是返回給Python的包裝對(duì)象,其內(nèi)部是PyCodeObject,PyCodeObject對(duì)象如下:

/* _Bytecode object_ */  
struct PyCodeObject {  
    PyObject_HEAD / _Python定長(zhǎng)對(duì)象頭_ /

    PyObject *co_consts;        /* 常量列表 */
    PyObject *co_names;         /* 名稱(chēng)列表(常量名、函數(shù)名、類(lèi)名等) */
    PyObject *co_code;          /* 指令操作碼(字節(jié)碼) */
    PyObject *co_filename;      /* 源文件名 */

    ...完整屬性請(qǐng)查看cpython/Include/cpython/code.h
};

查看result的屬性值:

>>> result.co_names
('range', 'i', 'print')
>>> result.co_consts
(3, None)
>>> result.co_code  
b'x\x18e\x00d\x00\x83\x01D\x00]\x0cZ\x01e\x02e\x01\x83\x01\x01\x00q\nW\x00d\x01S\x00'
>>> result.co_filename
''

其中co_code屬性是不可讀的bytes數(shù)據(jù),這時(shí)候就要用到Python內(nèi)置的一個(gè)模塊——dis,dis 模塊通過(guò)反匯編支持CPython的 bytecode 分析。
通過(guò)dis模塊的disco函數(shù),可反匯編代碼對(duì)象

  • disco(code, lasti=-1, ***, file=None)
    其中參數(shù)code是代碼對(duì)象

disco方法返回字節(jié)碼操作的格式化字符,描述了Python解釋器將要執(zhí)行的指令,內(nèi)容類(lèi)似于匯編語(yǔ)言,內(nèi)容分為以下幾列:

  1. 行號(hào),用于每行的第一條指令
  2. 當(dāng)前指令,表示為 --> ,
  3. 一個(gè)指令 >> 表示,
  4. 指令的地址,
  5. 操作碼名稱(chēng),
  6. 操作參數(shù),和括號(hào)中的參數(shù)解釋。

例如:

>>> import dis
>>> dis.disco(result)         
  1           0 SETUP_LOOP              24 (to 26)
              2 LOAD_NAME                0 (range)
              4 LOAD_CONST               0 (3)
              6 CALL_FUNCTION            1
              8 GET_ITER
        >>   10 FOR_ITER                12 (to 24)
             12 STORE_NAME               1 (i)
             14 LOAD_NAME                2 (print)
             16 LOAD_NAME                1 (i)
             18 CALL_FUNCTION            1
             20 POP_TOP
             22 JUMP_ABSOLUTE           10
        >>   24 POP_BLOCK
        >>   26 LOAD_CONST               1 (None)
             28 RETURN_VALUE

SETUP_LOOP 24 (to 26):將一個(gè)用于循環(huán)的塊推到塊堆棧上。該塊從當(dāng)前指令開(kāi)始,其大小為24字節(jié)
LOAD_NAME 0 (0):將result.co_names[0]的值移到棧頂,該值是'range'
LOAD_CONST 0 (0):將result.co_consts[0]移到棧頂,該值是3
CALL_FUNCTION 1:調(diào)用一個(gè)可調(diào)用對(duì)象并傳入位置參數(shù)1
POP_TOP:刪除棧頂元素,即刪除TOS
GET_ITER:實(shí)現(xiàn)TOS = iter(TOS),把 iter(TOS) 的結(jié)果推回堆棧
...

>>> result.co_names[0]  
'range'
>>> result.co_consts[0]  
3

綜上所述,Python內(nèi)建函數(shù)compile在編譯某段源碼時(shí),不會(huì)直接返回字節(jié)碼,而是返回一個(gè)CodeObject對(duì)象,該對(duì)象存儲(chǔ)了程序運(yùn)行所需的相關(guān)數(shù)據(jù)和程序運(yùn)行過(guò)程
更多操作碼解釋和dis模塊的使用請(qǐng)查看:dis --- Python 字節(jié)碼反匯編器

動(dòng)態(tài)編譯

Java程序運(yùn)行時(shí),會(huì)先編譯所有Java文件并加載類(lèi),程序運(yùn)行起來(lái)后無(wú)論修改或新增代碼,都不會(huì)影響程序運(yùn)行,如果需要在運(yùn)行中加入新的代碼,需要先調(diào)用編譯器(JavaCompiler)編譯代碼,再調(diào)用類(lèi)加載器(ClassLoader)將編譯后的字節(jié)碼加入當(dāng)前的運(yùn)行環(huán)境,這個(gè)過(guò)程就是動(dòng)態(tài)編譯。

在Python中,要實(shí)現(xiàn)程序運(yùn)行后執(zhí)行新增的代碼要簡(jiǎn)單得多,Python提供一個(gè)內(nèi)建函數(shù)exec,可以執(zhí)行代碼語(yǔ)句或者是經(jīng)過(guò)compile編譯后的代碼對(duì)象:

>>> help(exec)
Help on built-in function exec in module builtins:

exec(source, globals=None, locals=None, /)
    Execute the given source in the context of globals and locals.

    The source may be a string representing one or more Python statements
    or a code object as returned by compile().
    The globals must be a dictionary and locals can be any mapping,
    defaulting to the current globals and locals.
    If only globals is given, locals defaults to it.

所以,如果代碼比較簡(jiǎn)單,可以直接調(diào)用exec執(zhí)行Python語(yǔ)句字符串;如果代碼比較復(fù)雜,可以先寫(xiě)入.py文件,調(diào)用compile編譯代碼后再調(diào)用exec執(zhí)行。
綜上所述,最多只需要調(diào)用兩個(gè)方法,就可以實(shí)現(xiàn)Python的動(dòng)態(tài)編譯了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容