哈嘍大家好,我是咸魚
不知道有沒有小伙伴跟我一樣,剛開始學(xué)習(xí) Python 的時候都聽說過 Python 是一種解釋型語言,因?yàn)樗谶\(yùn)行的時候會逐行解釋并執(zhí)行,而 C++ 這種是編譯型語言
圖片
不過我今天看到了一篇文章,作者提出 Python 其實(shí)也有編譯的過程,解釋器會先編譯再執(zhí)行
不但如此,作者還認(rèn)為【解釋】與【編譯】是錯誤的二分法、限制了編程語言的可能性。Python 既是解釋型語言,也是編譯型語言!
本文文字較多,干貨滿滿,耐心看完相信你會有不小的收獲
原文:https://eddieantonio.ca/blog/2023/10/25/python-is-a-compiled-language/
本文所說的 Python ,不是指 PyPy、Mypyc、Numba、Cinder 等 Python 的替代版本,也不是像 Cython、Codon、mojo1這樣的類 Python 編程語言
我指的是常規(guī)的 Python——CPython
目前,我正在編寫一份教材,教學(xué)生如何閱讀和理解程序報錯信息(programming error messages)。我們正在為三種編程語言(C、Python、Java)開設(shè)課程
程序報錯信息的本質(zhì)的關(guān)鍵點(diǎn)之一在于程序報錯是在不同階段生成的,有些是在編譯時生成,有些是在運(yùn)行時生成
第一門課是針對 C 語言的,具體來說是如何使用 GCC 編譯器,以及演示 GCC 如何將代碼轉(zhuǎn)換成可執(zhí)行程序
除此之外,這節(jié)課還討論了在上述階段可能出現(xiàn)的程序報錯,以及這些報錯將如何影響所呈現(xiàn)的錯誤消息。重要的是:早期階段的錯誤將阻止在后期階段檢測到錯誤(也就是說 A 階段的報錯出現(xiàn)之后,B 階段就算有錯誤也不會檢測出來)
當(dāng)我將這門課調(diào)整成針對 Java 和 Python 時,我發(fā)現(xiàn) Python 和 Java 都沒有預(yù)處理器(preprocessor),并且 Python 和 Java 的鏈接(linking)不是同一個概念
我忽略了上面這些變化,但是我偶然發(fā)現(xiàn)了一個有趣的現(xiàn)象:
編譯器在各個階段會生成報錯信息,而且編譯器通常會在繼續(xù)執(zhí)行之前把前面階段的報錯顯示出來,這就意味著我們可以通過在程序中故意創(chuàng)建錯誤來發(fā)現(xiàn)編譯器的各個階段
所以讓我們玩一個小游戲來發(fā)現(xiàn) Python 解釋器的各個階段
我們將創(chuàng)建一個包含多個 bug 的 Python 程序,每個 bug 都試圖引發(fā)不同類型的報錯信息
我們知道常規(guī)的 Python 每次運(yùn)行只會報告一個錯誤,所以這個游戲就是——哪條報錯會被首先觸發(fā)
圖片
每行代碼都會產(chǎn)生不同的報錯:
問題在于,哪個錯誤會先被顯示出來?需要注意的是:Python 版本很重要(比我想象的要重要),所以如果你看到不同的結(jié)果,請記住這一點(diǎn)
PS:下面運(yùn)行代碼所使用的 Python 版本為 Python 3.12
在開始執(zhí)行代碼之前,先想想【解釋】語言和【編譯】語言對你來說意味著什么?
下面我將給出一段蘇格拉底式的對話,希望你能反思一下其中的區(qū)別
蘇格拉底:編譯語言是指代碼在運(yùn)行之前首先通過編譯器的語言。一個例子是 C 編程語言。要運(yùn)行 C 代碼,首先必須運(yùn)行像 or clang 這樣的 gcc 編譯器,然后才能運(yùn)行代碼。編譯后的語言被轉(zhuǎn)換為機(jī)器代碼,即 CPU 可以理解的 1 和 0。
柏拉圖:等等,Java不是一種編譯語言嗎?
蘇格拉底:是的,Java是一種編譯語言。
柏拉圖:但是常規(guī) Java編譯器的輸出不是一個 .class 文件。那是字節(jié)碼,不是嗎?
蘇格拉底:沒錯。字節(jié)碼不是機(jī)器碼,但 Java 仍然是一種編譯語言。這是因?yàn)榫幾g器可以捕獲許多問題,因此您需要在程序開始運(yùn)行之前更正錯誤。
柏拉圖:解釋型語言呢?
蘇格拉底:解釋型語言是依賴于一個單獨(dú)的程序(恰如其分地稱為解釋器)來實(shí)際運(yùn)行代碼的語言。解釋型語言不需要程序員先運(yùn)行編譯器。因此,在程序運(yùn)行時,您犯的任何錯誤都會被捕獲。Python 是一種解釋型語言,沒有單獨(dú)的編譯器,您犯的所有錯誤都會在運(yùn)行時捕獲。
柏拉圖:如果 Python不是一種編譯語言,那么為什么標(biāo)準(zhǔn)庫包含名為 py_compile and compileall 的模塊?
蘇格拉底:嗯,這些模塊只是將 Python轉(zhuǎn)換為字節(jié)碼。他們不會將 Python 轉(zhuǎn)換為機(jī)器代碼,因此 Python 仍然是一種解釋型語言。
柏拉圖:那么,Python和 Java都轉(zhuǎn)換為字節(jié)碼了嗎?
蘇格拉底:對。
柏拉圖:那么,為什么Python是一種解釋型語言,而 Java卻是一種編譯型語言呢?
蘇格拉底:因?yàn)?Python 中的所有錯誤都是在運(yùn)行時捕獲的。 (ps:請注意這句話)
當(dāng)我們執(zhí)行上面那段有 bug 的程序時,將會收到下面的錯誤
圖片
檢測到的第一個報錯位于源碼的最后一行??梢钥吹剑涸谶\(yùn)行第一行代碼之前,Python 必須讀取整個源碼文件
如果你腦子里有一個關(guān)于【解釋型語言】的定義,其中包括”解釋型語言按順序讀取代碼,一次運(yùn)行一行”,我希望你忘掉它
我還沒有深入研究 CPython 解釋器的源碼來驗(yàn)證這一點(diǎn),但我認(rèn)為這是第一個檢測到的報錯的原因是 Python 3.12 所做的第一個步驟是掃描(scanning ),也稱為詞法分析
掃描器將整個文件轉(zhuǎn)換為一系列標(biāo)記(token),然后繼續(xù)進(jìn)行下一階段。
掃描器掃描到源碼最后一行的字符串字面值末尾少了個引號,它希望把整個字符串字面值轉(zhuǎn)換成一個 token ,但是沒有結(jié)束引號它就轉(zhuǎn)換不了
在 Python 3.12 中,掃描器首先運(yùn)行,所以這也是為什么第一個報錯是unterminated string literal
我們把第四行的代碼的 bug 修復(fù)好,第 1 2 3 行仍有 bug
圖片
我們現(xiàn)在來執(zhí)行代碼,看下哪個會首先報錯
圖片
這次是第二行報錯!同樣,我沒有去查看 CPython 的源碼,但是我有理由確定掃描的下一階段是解析(parsing),也稱為語法分析
在運(yùn)行代碼之前會先解析源碼,這意味著 Python 不會看到第一行的錯誤,而是在第二行報錯
我要指出我為這個小游戲而編寫的代碼是完全沒有意義的,并且對于如何修復(fù) bug 也沒有正確的答案。我的目的純粹是編寫錯誤然后發(fā)現(xiàn) python 解釋器現(xiàn)在處在哪個階段
我不知道 print() = None可能是什么意思,所以我將通過將其替換為print(None)來解決這個問題,這也沒有意義,但至少它在語法上是正確的。
我們把第二行的語法錯誤也修復(fù)了,但源碼還有另外兩個錯誤,其中一個也是語法錯誤
圖片
回想一下,語法錯誤在回合二的時候優(yōu)先顯示了出來,在回合三還會一樣嗎
圖片
沒錯!第三行的語法錯誤優(yōu)先于第一行的錯誤
正如回合二一樣,Python 解釋器在運(yùn)行代碼之前會先解析源碼,對其進(jìn)行語法分析
這意味著 Python 不會看到第一行的錯誤,而是在第三行報錯
你可能想知道為什么我在一個文件中插入了兩個 SyntaxError,難道一個還不夠表明我的觀點(diǎn)嗎?
這是因?yàn)?Python 版本的不同會導(dǎo)致結(jié)果的不同,如果你在 Python3.8 或者更早的版本去運(yùn)行代碼,那么結(jié)果如下
在 Python 3.8 中,第 2 輪報告的第一個錯誤消息位于第 3 行:
圖片
修復(fù)第三行的錯誤之后,Python 3.8 在第 2 行報告以下錯誤消息:
圖片
為什么 Python 3.8 和 3.12 報錯順序不一樣?是因?yàn)?Python 3.9 引入了一個新的解析器。這個解析器比以前的 na?ve 解析器功能更強(qiáng)大
舊的解析器無法提前查看多個 token,這意味著舊解析器在技術(shù)上可以接受語法無效的 Python 程序
尤其是這種限制導(dǎo)致解析器無法識別賦值語句的左邊是否為有效的賦值目標(biāo),好比下面這段代碼,舊解析器能夠接收下面的代碼
圖片
上面這段代碼沒有任何意義,甚至 Python 語法是不允許這么使用的。為了解決這個問題,Python 曾經(jīng)存在過一個獨(dú)立的,hacky 的階段(這個 hacky 我不知道用什么翻譯比較好)
即 Python會檢查所有的賦值語句,并確保賦值號左邊實(shí)際上是可以被賦值的東西
而這個階段是發(fā)生在解析之后,這也就是為什么舊版本 Python 中會先把第二行的報錯先顯示出來
現(xiàn)在還剩最后一個錯誤了
圖片
我們來運(yùn)行一下
圖片
需要注意的是,Traceback (most recent call last)表示 Python 運(yùn)行時報錯的主要內(nèi)容,這里在回合四才出現(xiàn)
經(jīng)過前面的掃描、解析階段,Python 終于能夠運(yùn)行代碼了。但是當(dāng) Python 開始運(yùn)行解釋第一行的時候,引發(fā)一個名為 ZeroDivisionError 的報錯
為什么知道現(xiàn)在處于【運(yùn)行時】,因?yàn)?Python 已經(jīng)打印出 Traceback (most recent call last),這表示我們有一個堆棧跟蹤
堆棧跟蹤只能在運(yùn)行時存在,這意味著這個報錯必須在運(yùn)行時捕獲。
但這意味著在回合1~3 中遇到的報錯不是運(yùn)行時報錯,那它們是什么報錯?
沒錯!CPython 解釋器實(shí)際上是一個解釋器,但它也是一個編譯器
我希望上面的練習(xí)已經(jīng)說明了 Python 在運(yùn)行第一行代碼之前必須經(jīng)過幾個階段:
舊版本的 Python 多了一個額外階段:
讓我們將其與前面編譯 C 程序的階段進(jìn)行比較:
Python 在運(yùn)行任何代碼之前仍然執(zhí)行一些編譯階段,就像 Java一樣,它會把源碼編譯成字節(jié)碼
前面三個報錯是 Python 在編譯階段產(chǎn)生的,只有最后一個才是在運(yùn)行時產(chǎn)生,即ZeroDivisionError: division by zero.
實(shí)際上,我們可以使用命令行上的 compileall 模塊預(yù)先編譯所有 Python 代碼:
圖片
這會將當(dāng)前目錄中所有 Python 文件的編譯字節(jié)碼放入其中 __pycache__/ ,并顯示任何編譯器錯誤
如果你想知道那個 __pycache__/ 文件夾中到底有什么,我為 EdmontonPy 做了一個演講,你應(yīng)該看看!
演講地址:https://www.youtube.com/watch?v=5yqUTJuFuUk&t=7m11s
只有在 Python 被編譯為字節(jié)碼之后,解釋器才會真正啟動,我希望前面的練習(xí)已經(jīng)證明 Python 確實(shí)可以在運(yùn)行時之前報錯
每當(dāng)一種編程語言被歸類為【編譯】或【解釋】語言時,我都會感到很討厭。一種語言本身不是編譯或解釋的
一種語言是編譯還是解釋(或兩者兼而有之?。┦且粋€實(shí)現(xiàn)細(xì)節(jié)
我不是唯一一個有這種想法的人。Laurie Tratt 有一篇精彩的文章,通過編寫一個逐漸成為優(yōu)化編譯器的解釋器來論證這一點(diǎn)
文章地址:https://tratt/laurie/blog/2023/compiled_and_interpreted_languages_two_ways_of_saying_tomato.html
還有一篇文章就是 Bob Nystrom 的 Crafting Interpreters。以下是第 2 章的一些引述:
事實(shí)證明,這就像問水果和蔬菜之間的區(qū)別一樣。這似乎是一個二元的非此即彼的選擇,但實(shí)際上“水果”是一個植物學(xué)術(shù)語,而“蔬菜”是烹飪學(xué)術(shù)語。
嚴(yán)格來說,一個并不意味著對另一個的否定。有些水果不是蔬菜(蘋果),有些蔬菜不是水果(胡蘿卜),但也有既是水果又是蔬菜的可食用植物,如西紅柿
當(dāng)你使用 CPython 來運(yùn)行 Python 程序時,源碼會被解析并轉(zhuǎn)換成內(nèi)部字節(jié)碼格式,然后在虛擬機(jī)中執(zhí)行
從用戶的角度來看,這顯然是一個解釋器(因?yàn)樗鼈儚脑创a運(yùn)行程序),但如果你仔細(xì)觀察 CPython(Python 也可譯作蟒蛇)的鱗狀表皮(scaly skin),你會發(fā)現(xiàn)它肯定在進(jìn)行編譯
答案是:CPython 是一個解釋器,它有一個編譯器
那么為什么這很重要呢?為什么在【編譯】和【解釋】語言之間做出嚴(yán)格的區(qū)分會適得其反?
【編譯】與【解釋】限制了我們認(rèn)為編程語言的可能性
編程語言不必由它是編譯還是解釋來定義的!以這種僵化的方式思考限制了我們認(rèn)為給定的編程語言可以做的事情
例如,JavaScript 通常被歸入“解釋型語言”類別。但有一段時間,在 Google Chrome 中運(yùn)行的 JavaScript 永遠(yuǎn)不會被解釋——相反,JavaScript 被直接編譯為機(jī)器代碼!因此,JavaScript 可以跟上 C++ 的步伐
出于這個原因,我真的厭倦了那些說解釋型語言必然慢的論點(diǎn)——性能是多方面的,并且不僅僅取決于"默認(rèn)"編程語言的實(shí)現(xiàn)
JavaScript 現(xiàn)在很快了、Ruby 現(xiàn)在很快了、Lua 已經(jīng)快了一段時間了
那對于通常被標(biāo)記為編譯型語言的編程語言呢?(例如 C)你是不會去想著解釋 C 語言程序的
語言之間真正的區(qū)別
語言之間真正的區(qū)別:【靜態(tài)】還是【動態(tài)】
我們應(yīng)該教給學(xué)生的真正區(qū)別是語言特性的區(qū)別,前者可以靜態(tài)地確定,即只盯著代碼而不運(yùn)行代碼,后者只能在運(yùn)行時動態(tài)地知道
需要注意的是,我說的是【語言特性】而不是【語言】,每種編程語言都選擇自己的一組屬性,這些屬性可以靜態(tài)地或動態(tài)地確定,并結(jié)合在一起,這使得語言更“動態(tài)”或更“靜態(tài)”
靜態(tài)與動態(tài)是一個范圍,Python 位于范圍中更動態(tài)的一端。像 Java 這樣的語言比 Python 有更多的靜態(tài)特性,但即使是 Java 也包括反射之類的東西,這無疑是一種動態(tài)特性
我發(fā)現(xiàn)動態(tài)與靜態(tài)經(jīng)常被混為一談,編譯與解釋混為一談,這是可以理解的
因?yàn)橥ǔJ褂媒忉屍鞯恼Z言具有更多的動態(tài)特性,如 Python、Ruby 和 JavaScript
具有更多靜態(tài)特性的語言往往在沒有解釋器的情況下實(shí)現(xiàn),例如 C++ 和 Rust
然后是介于兩者之間的 Java
Python 中的靜態(tài)類型注釋已經(jīng)逐漸(呵呵)在代碼庫中得到采用,其中一個期望是:由于更多靜態(tài)的東西,這可以解鎖 Python 代碼中的性能優(yōu)勢
不幸的是,事實(shí)證明,Python 中的類型(是的,只是一般類型,考慮元類)和注釋本身都是Python 的動態(tài)特性,這使得靜態(tài)類型不是大伙所期望的性能優(yōu)勢
最后總結(jié)一下:
本文鏈接:http://www.www897cc.com/showinfo-26-17772-0.htmlPython 既是解釋型語言,也是編譯型語言
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權(quán)等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。郵件:2376512515@qq.com