<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
我們每天都要編寫一些Python程式,或者用來處理一些文字,或者是做一些系統管理工作。程式寫好後,只需要敲下python命令,便可將程式啟動起來並開始執行:
$ python some-program.py
那麼,一個文字形式的.py檔案,是如何一步步轉換為能夠被CPU執行的機器指令的呢?此外,程式執行過程中可能會有.pyc檔案生成,這些檔案又有什麼作用呢?
雖然從行為上看Python更像Shell指令碼這樣的解釋性語言,但實際上Python程式執行原理本質上跟Java或者C#一樣,都可以歸納為虛擬機器器和位元組碼。Python執行程式分為兩步:先將程式程式碼編譯成位元組碼,然後啟動虛擬機器器執行位元組碼:
雖然Python命令也叫做Python直譯器,但跟其他指令碼語言直譯器有本質區別。實際上,Python直譯器包含編譯器以及虛擬機器器兩部分。當Python直譯器啟動後,主要執行以下兩個步驟:
編譯器將.py檔案中的Python原始碼編譯成位元組碼虛擬機器器逐行執行編譯器生成的位元組碼
因此,.py檔案中的Python語句並沒有直接轉換成機器指令,而是轉換成Python位元組碼。
Python程式的編譯結果是位元組碼,裡面有很多關於Python執行的相關內容。因此,不管是為了更深入理解Python虛擬機器器執行機制,還是為了調優Python程式執行效率,位元組碼都是關鍵內容。那麼,Python位元組碼到底長啥樣呢?我們如何才能獲得一個Python程式的位元組碼呢——Python提供了一個內建函數compile用於即時編譯原始碼。我們只需將待編譯原始碼作為引數呼叫compile函數,即可獲得原始碼的編譯結果。
下面,我們通過compile函數來編譯一個程式:
原始碼儲存在demo.py檔案中:
PI = 3.14 def circle_area(r): return PI * r ** 2 class Person(object): def __init__(self, name): self.name = name def say(self): print('i am', self.name)
編譯之前需要將原始碼從檔案中讀取出來:
>>> text = open('D:myspacecodepythonCodemixdemo.py').read() >>> print(text) PI = 3.14 def circle_area(r): return PI * r ** 2 class Person(object): def __init__(self, name): self.name = name def say(self): print('i am', self.name)
然後呼叫compile函數來編譯原始碼:
>>> result = compile(text,'D:myspacecodepythonCodemixdemo.py', 'exec')
compile函數必填的引數有3個:
source:待編譯原始碼
filename:原始碼所在檔名
mode:編譯模式,exec表示將原始碼當作一個模組來編譯
exec:用於編譯模組原始碼
single:用於編譯一個單獨的Python語句(互動式下)
eval:用於編譯一個eval表示式
通過compile函數,我們獲得了最後的原始碼編譯結果result:
>>> result <code object <module> at 0x000001DEC2FCF680, file "D:myspacecodepythonCodemixdemo.py", line 1> >>> result.__class__ <class 'code'>
最終我們得到了一個code型別的物件,它對應的底層結構體是PyCodeObject
PyCodeObject原始碼如下:
/* Bytecode object */ struct PyCodeObject { PyObject_HEAD int co_argcount; /* #arguments, except *args */ int co_posonlyargcount; /* #positional only arguments */ int co_kwonlyargcount; /* #keyword only arguments */ int co_nlocals; /* #local variables */ int co_stacksize; /* #entries needed for evaluation stack */ int co_flags; /* CO_..., see below */ int co_firstlineno; /* first source line number */ PyObject *co_code; /* instruction opcodes */ PyObject *co_consts; /* list (constants used) */ PyObject *co_names; /* list of strings (names used) */ PyObject *co_varnames; /* tuple of strings (local variable names) */ PyObject *co_freevars; /* tuple of strings (free variable names) */ PyObject *co_cellvars; /* tuple of strings (cell variable names) */ /* The rest aren't used in either hash or comparisons, except for co_name, used in both. This is done to preserve the name and line number for tracebacks and debuggers; otherwise, constant de-duplication would collapse identical functions/lambdas defined on different lines. */ Py_ssize_t *co_cell2arg; /* Maps cell vars which are arguments. */ PyObject *co_filename; /* unicode (where it was loaded from) */ PyObject *co_name; /* unicode (name, for reference) */ PyObject *co_linetable; /* string (encoding addr<->lineno mapping) See Objects/lnotab_notes.txt for details. */ void *co_zombieframe; /* for optimization only (see frameobject.c) */ PyObject *co_weakreflist; /* to support weakrefs to code objects */ /* Scratch space for extra data relating to the code object. Type is a void* to keep the format private in codeobject.c to force people to go through the proper APIs. */ void *co_extra; /* Per opcodes just-in-time cache * * To reduce cache size, we use indirect mapping from opcode index to * cache object: * cache = co_opcache[co_opcache_map[next_instr - first_instr] - 1] */ // co_opcache_map is indexed by (next_instr - first_instr). // * 0 means there is no cache for this opcode. // * n > 0 means there is cache in co_opcache[n-1]. unsigned char *co_opcache_map; _PyOpcache *co_opcache; int co_opcache_flag; // used to determine when create a cache. unsigned char co_opcache_size; // length of co_opcache. };
程式碼物件PyCodeObject用於儲存編譯結果,包括位元組碼以及程式碼涉及的常數、名字等等。關鍵欄位包括:
欄位 | 用途 |
---|---|
co_argcount | 引數個數 |
co_kwonlyargcount | 關鍵字引數個數 |
co_nlocals | 區域性變數個數 |
co_stacksize | 執行程式碼所需棧空間 |
co_flags | 標識 |
co_firstlineno | 程式碼塊首行行號 |
co_code | 指令操作碼,即位元組碼 |
co_consts | 常數列表 |
co_names | 名字列表 |
co_varnames | 區域性變數名列表 |
下面列印看一下這些欄位對應的資料:
通過co_code欄位獲得位元組碼:
>>> result.co_code b'dx00Zx00dx01dx02x84x00Zx01Gx00dx03dx04x84x00dx04ex02x83x03Zx03dx05Sx00'
通過co_names欄位獲得程式碼物件涉及的所有名字:
>>> result.co_names ('PI', 'circle_area', 'object', 'Person')
通過co_consts欄位獲得程式碼物件涉及的所有常數:
>>> result.co_consts (3.14, <code object circle_area at 0x0000023D04D3F310, file "D:myspacecodepythonCodemixdemo.py", line 3>, 'circle_area', <code object Person at 0x0000023D04D3F5D0, file "D:myspacecodepythonCodemixdemo.py", line 6>, 'Person', None)
可以看到,常數列表中還有兩個程式碼物件,其中一個是circle_area函數體,另一個是Person類定義體。對應Python中作用域的劃分方式,可以自然聯想到:每個作用域對應一個程式碼物件。如果這個假設成立,那麼Person程式碼物件的常數列表中應該還包括兩個程式碼物件:init函數體和say函數體。下面取出Person類程式碼物件來看一下:
>>> person_code = result.co_consts[3] >>> person_code <code object Person at 0x0000023D04D3F5D0, file "D:myspacecodepythonCodemixdemo.py", line 6> >>> person_code.co_consts ('Person', <code object __init__ at 0x0000023D04D3F470, file "D:myspacecodepythonCodemixdemo.py", line 7>, 'Person.__init__', <code object say at 0x0000023D04D3F520, file "D:myspacecodepythonCodemixdemo.py", line 10>, 'Person.say', None)
因此,我們得出結論:Python原始碼編譯後,每個作用域都對應著一個程式碼物件,子作用域程式碼物件位於父作用域程式碼物件的常數列表裡,層級一一對應。
至此,我們對Python原始碼的編譯結果——程式碼物件PyCodeObject有了最基本的認識,後續會在虛擬機器器、函數機制、類機制中進一步學習。
位元組碼是一串不可讀的位元組序列,跟二進位制機器碼一樣。如果想讀懂機器碼,可以將其反組合,那麼位元組碼可以反編譯嗎?
通過dis模組可以將位元組碼反編譯:
>>> import dis >>> dis.dis(result.co_code) 0 LOAD_CONST 0 (0) 2 STORE_NAME 0 (0) 4 LOAD_CONST 1 (1) 6 LOAD_CONST 2 (2) 8 MAKE_FUNCTION 0 10 STORE_NAME 1 (1) 12 LOAD_BUILD_CLASS 14 LOAD_CONST 3 (3) 16 LOAD_CONST 4 (4) 18 MAKE_FUNCTION 0 20 LOAD_CONST 4 (4) 22 LOAD_NAME 2 (2) 24 CALL_FUNCTION 3 26 STORE_NAME 3 (3) 28 LOAD_CONST 5 (5) 30 RETURN_VALUE
位元組碼反編譯後的結果和組合語言很類似。其中,第一列是位元組碼的偏移量,第二列是指令,第三列是運算元。以第一條位元組碼為例,LOAD_CONST指令將常數載入進棧,常數下標由運算元給出,而下標為0的常數是:
>>> result.co_consts[0]3.14
這樣,第一條位元組碼的意義就明確了:將常數3.14載入到棧。
由於程式碼物件儲存了位元組碼、常數、名字等上下文資訊,因此直接對程式碼物件進行反編譯可以得到更清晰的結果:
>>>dis.dis(result) 1 0 LOAD_CONST 0 (3.14) 2 STORE_NAME 0 (PI) 3 4 LOAD_CONST 1 (<code object circle_area at 0x0000023D04D3F310, file "D:myspacecodepythonCodemixdemo.py", line 3>) 6 LOAD_CONST 2 ('circle_area') 8 MAKE_FUNCTION 0 10 STORE_NAME 1 (circle_area) 6 12 LOAD_BUILD_CLASS 14 LOAD_CONST 3 (<code object Person at 0x0000023D04D3F5D0, file "D:myspacecodepythonCodemixdemo.py", line 6>) 16 LOAD_CONST 4 ('Person') 18 MAKE_FUNCTION 0 20 LOAD_CONST 4 ('Person') 22 LOAD_NAME 2 (object) 24 CALL_FUNCTION 3 26 STORE_NAME 3 (Person) 28 LOAD_CONST 5 (None) 30 RETURN_VALUE Disassembly of <code object circle_area at 0x0000023D04D3F310, file "D:myspacecodepythonCodemixdemo.py", line 3>: 4 0 LOAD_GLOBAL 0 (PI) 2 LOAD_FAST 0 (r) 4 LOAD_CONST 1 (2) 6 BINARY_POWER 8 BINARY_MULTIPLY 10 RETURN_VALUE Disassembly of <code object Person at 0x0000023D04D3F5D0, file "D:myspacecodepythonCodemixdemo.py", line 6>: 6 0 LOAD_NAME 0 (__name__) 2 STORE_NAME 1 (__module__) 4 LOAD_CONST 0 ('Person') 6 STORE_NAME 2 (__qualname__) 7 8 LOAD_CONST 1 (<code object __init__ at 0x0000023D04D3F470, file "D:myspacecodepythonCodemixdemo.py", line 7>) 10 LOAD_CONST 2 ('Person.__init__') 12 MAKE_FUNCTION 0 14 STORE_NAME 3 (__init__) 10 16 LOAD_CONST 3 (<code object say at 0x0000023D04D3F520, file "D:myspacecodepythonCodemixdemo.py", line 10>) 18 LOAD_CONST 4 ('Person.say') 20 MAKE_FUNCTION 0 22 STORE_NAME 4 (say) 24 LOAD_CONST 5 (None) 26 RETURN_VALUE Disassembly of <code object __init__ at 0x0000023D04D3F470, file "D:myspacecodepythonCodemixdemo.py", line 7>: 8 0 LOAD_FAST 1 (name) 2 LOAD_FAST 0 (self) 4 STORE_ATTR 0 (name) 6 LOAD_CONST 0 (None) 8 RETURN_VALUE Disassembly of <code object say at 0x0000023D04D3F520, file "D:myspacecodepythonCodemixdemo.py", line 10>: 11 0 LOAD_GLOBAL 0 (print) 2 LOAD_CONST 1 ('i am') 4 LOAD_FAST 0 (self) 6 LOAD_ATTR 1 (name) 8 CALL_FUNCTION 2 10 POP_TOP 12 LOAD_CONST 0 (None) 14 RETURN_VALUE
運算元指定的常數或名字的實際值在旁邊的括號內列出,此外,位元組碼以語句為單位進行了分組,中間以空行隔開,語句的行號在位元組碼前面給出。例如PI = 3.14這個語句就被會變成了兩條位元組碼:
1 0 LOAD_CONST 0 (3.14) 2 STORE_NAME 0 (PI)
如果將demo作為模組匯入,Python將在demo.py檔案所在目錄下生成.pyc檔案:
>>> import demo
pyc檔案會儲存經過序列化處理的程式碼物件PyCodeObject。這樣一來,Python後續匯入demo模組時,直接讀取pyc檔案並反序列化即可得到程式碼物件,避免了重複編譯導致的開銷。只有demo.py有新修改(時間戳比.pyc檔案新),Python才會重新編譯。
因此,對比Java而言:Python中的.py檔案可以類比Java中的.java檔案,都是原始碼檔案;而.pyc檔案可以類比.class檔案,都是編譯結果。只不過Java程式需要先用編譯器javac命令來編譯,再用虛擬機器器java命令來執行;而Python直譯器把這兩個過程都完成了。
以上就是Python位元組碼與程式執行過程詳解的詳細內容,更多關於Python程式執行位元組碼的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45