Python語言通常被看作是解釋型語言,不同于像C語言那樣的編譯型。但實(shí)際上,如果說Python是編譯型語言,也未嘗不可。我們來一起看一下1! 1.舉個(gè)栗子 首先看一個(gè)簡單的例子: #!/usr/bin/python3# file name :demo1.py a=1 b=2print('a+b = ',a+b) c=NotDefinedValue print(c) 這里第四行有個(gè)賦值的錯(cuò)誤,但python在運(yùn)行前不會(huì)進(jìn)行類型檢查,所以該程序仍可正常運(yùn)行,直至遇到錯(cuò)誤,運(yùn)行結(jié)果與預(yù)想的一致: a+b =3 Traceback (most recent call last): File '/demo.py', line 4,inc=NotDefinedValue NameError: name 'NotDefinedValue'isnot defined Process finished with exit code 1 現(xiàn)在稍微改動(dòng)一下,使最后一行有個(gè)語法錯(cuò)誤(少個(gè)括號(hào)): #!/usr/bin/python3# file name :demo2.py a=1 b=2print('a+b = ',a+b) c=NotDefinedValue print(c 按照對(duì)python語言的理解,程序應(yīng)該會(huì)逐行執(zhí)行,直至遇到第一個(gè)賦值語句的錯(cuò)誤,然后拋出異常。執(zhí)行結(jié)果應(yīng)該和上面的例子一樣。是不是這樣呢,我們?cè)囍鴪?zhí)行,結(jié)果如下: File '/demo.py', line 6 SyntaxError: unexpected EOF while parsing 可見沒有像預(yù)想的一樣,而是直接拋出語法錯(cuò)誤。 那么問題來了,前三行代碼沒錯(cuò)誤,為什么不能正常執(zhí)行呢?python作為解釋性語言,應(yīng)該是“一邊執(zhí)行一邊轉(zhuǎn)換”的,后面的“錯(cuò)誤”按理說不會(huì)影響前面正確的代碼的???2 那可能有同學(xué)要說了,python在運(yùn)行之前會(huì)檢查語法!但“檢查語法”是個(gè)怎樣的過程呢?要知道答案,需要了解python底層的運(yùn)作。 2. python運(yùn)行機(jī)制 我們都知道python “解釋器”(interpreter)這個(gè)東西,就是負(fù)責(zé)執(zhí)行python源碼的,大體的過程是這樣: 點(diǎn)擊加載圖片 對(duì)于解釋器內(nèi)部,可以分成兩部分:編譯器(compiler)和虛擬機(jī)(virtual machine),編譯器負(fù)責(zé)將源碼編譯成字節(jié)碼(byte code),字節(jié)碼交給虛擬機(jī)運(yùn)行,虛擬機(jī)會(huì)調(diào)用CPU內(nèi)存等硬件資源,進(jìn)行計(jì)算,最后產(chǎn)生結(jié)果。 點(diǎn)擊加載圖片 可見編譯器做了很多事情:生成語法樹(parse tree generation),生成AST(Abstract Syntax Tree),字節(jié)碼生成與優(yōu)化,最后產(chǎn)生字節(jié)碼對(duì)象。字節(jié)碼對(duì)象交給虛擬機(jī)后,虛擬機(jī)會(huì)讀取其中的指令,逐步執(zhí)行。了解java的同學(xué)會(huì)發(fā)現(xiàn),這個(gè)過程與java很類似,java源碼也是先編譯成字節(jié)碼(.class文件),后由JVM執(zhí)行。 這里能看到,語法分析在編譯階段就會(huì)進(jìn)行,此時(shí)若有語法錯(cuò)誤,則編譯不通過,拋出錯(cuò)誤;既然沒有編譯產(chǎn)生的字節(jié)碼,虛擬機(jī)自然也就不會(huì)執(zhí)行指令,也就不會(huì)有輸出結(jié)果。 所以上面第一個(gè)例子是可以正常編譯的,生成字節(jié)碼并交給虛擬機(jī),虛擬機(jī)執(zhí)行指令時(shí)會(huì)檢查類型,正確的指令會(huì)執(zhí)行,不對(duì)的就報(bào)錯(cuò);對(duì)于第二個(gè)例子,編譯器在分析語法的時(shí)候就發(fā)現(xiàn)錯(cuò)誤,停止編譯。所以兩個(gè)程序中斷的原因有著本質(zhì)不同。 3. *.pyc文件 看到python也有“字節(jié)碼”,可能又有同學(xué)有問題了:Java的字節(jié)碼存在于*.class文件中,那pyhton的字節(jié)碼在哪呢?答案就是 *.pyc文件。 我們有時(shí)候會(huì)在python源碼的文件夾中發(fā)現(xiàn)__pycache__文件夾3,里面就有*.pyc文件,這可能是自動(dòng)生成的。我們也可以手動(dòng)編譯源碼,生成*.pyc: python -m py_compile filename.py 我們先對(duì)第一個(gè)例子進(jìn)行編譯: python -m py_compile demo1.py 編譯通過,并在該文件目錄下有個(gè)__pycache__文件夾,進(jìn)入會(huì)發(fā)現(xiàn)demo1.cpython-37.pyc文件,這就是字節(jié)碼文件 4。這是供機(jī)器讀的二進(jìn)制文件(雖然這里是虛擬機(jī)),可以用hexdump(在Linux環(huán)境下)打開,結(jié)果以16進(jìn)制顯示: 點(diǎn)擊加載圖片 emm,雖然看上去很復(fù)雜,實(shí)際上確實(shí)很復(fù)雜。不過沒關(guān)系,可以嘗試著解讀一下。字節(jié)碼的前兩個(gè)4字節(jié)是魔術(shù)數(shù),是有關(guān)于版本號(hào)的,第三個(gè)4字節(jié)是時(shí)間戳,第四個(gè)4字節(jié)是源文件大小。在本例中,前兩個(gè)字節(jié)是420d0d0a000000005,略過;第三個(gè)4字節(jié)是d75a915e,因?yàn)槭切《四J?,?shí)際是5e915ad7,轉(zhuǎn)換成十進(jìn)制就是1586584279,這就很眼熟了,就是UNIX時(shí)間戳(時(shí)間為2020/4/11 13:51:19);接著后面的4字節(jié)是37000000,實(shí)際是00000037,十進(jìn)制就是55,也就是說源文件大小為55字節(jié)。通過查看文件屬性,也確實(shí)如此。 至于后面的部分,我們可以通過python的dis工具來查看: #/usr/bin/python3#file name:read_file.pyimport dis import marshal import sys defshow_file(fname:str)- >None:withopen(fname,'rb')as f: f.read(16)# pop the first 16 bytes dis.disassemble(marshal.loads(f.read))if __name__ =='__main__': show_file(sys.argv[1]) 這段代碼我們只輸出字節(jié)碼中的指令,其余部分略過。運(yùn)行,傳入pyc文件,結(jié)果如下: $ python3 read_file.py ./demo1.cpython-37.pyc 1 0 LOAD_CONST 0 (1) 2 STORE_NAME 0 (a) 2 4 LOAD_CONST 1 (2) 6 STORE_NAME 1 (b) 3 8 LOAD_NAME 2 (print) 10 LOAD_CONST 2 ('a+b = ') 12 LOAD_NAME 0 (a) 14 LOAD_NAME 1 (b) 16 BINARY_ADD 18 CALL_FUNCTION 2 20 POP_TOP 4 22 LOAD_NAME 3 (NotDefinedValue) 24 STORE_NAME 4 (c) 5 26 LOAD_NAME 2 (print) 28 LOAD_NAME 4 (c) 30 CALL_FUNCTION 1 32 POP_TOP 34 LOAD_CONST 3 (None) 36 RETURN_VALUE 看起來有點(diǎn)像匯編?那就對(duì)了,因?yàn)閐is模塊就是反匯編(disassemble),將(虛擬機(jī)的)機(jī)器碼反匯編成匯編。這里展示的是指令部分6,能看到源代碼的變量,值,函數(shù)等在棧上的壓入與彈出。具體每個(gè)指令什么意思這里就不再展開。 .pyc文件是交給虛擬機(jī)執(zhí)行的,所以我們可以運(yùn)行pyc文件,就像運(yùn)行普通py文件一樣: $ python3 demo1.cpython-37.pyc a+b = 3 Traceback (most recent call last): File 'demo1.py', line 4, in NameError: name 'NotDefinedValue' is not defined 沒問題,跟第一個(gè)例子運(yùn)行結(jié)果完全一樣。 至于第二個(gè)例子,編譯時(shí)就會(huì)出錯(cuò): $ python3 -m py_compile demo2.py File 'demo1.py', line 5 print(c ^ SyntaxError: unexpected EOF while parsing 4.小結(jié) 通過對(duì)python運(yùn)行機(jī)制的簡單探討,可以發(fā)現(xiàn)python其實(shí)并不是嚴(yán)格意義上的解釋型語言。實(shí)際上,解釋型與編譯型本身就沒有嚴(yán)格的定義,現(xiàn)在很多語言也在模糊這兩者的界限。 我們也沒必要糾結(jié)于具體是哪種類型的語言,這根本不重要。了解語言背后的機(jī)制,知道從輸入到輸出中間發(fā)生了什么,這才是更有意義的。 5.參考資料: Inside The Python Virtual Machine 海納.自己動(dòng)手寫python虛擬機(jī).北京航空航天大學(xué)出版社 Reading pyc file python文檔 [注] 本文用的python均為Cpython ?? 比如像shell script,后面的錯(cuò)誤確實(shí)不會(huì)影響前面代碼的執(zhí)行 ?? 什么時(shí)候生成__pycache文件有一定的規(guī)則,這里不贅述 ?? 準(zhǔn)確來說,pyc文件是字節(jié)碼對(duì)象在磁盤中持久化的結(jié)果 ?? 由于是16進(jìn)制,所以兩位就是2進(jìn)制的8位,也就是一個(gè)字節(jié) ?? 實(shí)際上pyc文件中有很多內(nèi)容,這里為簡單起見只查看了指令相關(guān)的內(nèi)容 ?? |
|