Python 既是解釋型語言,也是編譯型語言

来源:https://www.cnblogs.com/edisonfish/archive/2023/11/08/17816746.html
-Advertisement-
Play Games

哈嘍大家好,我是鹹魚 不知道有沒有小伙伴跟我一樣,剛開始學習 Python 的時候都聽說過 Python 是一種解釋型語言,因為它在運行的時候會逐行解釋並執行,而 C++ 這種是編譯型語言 不過我今天看到了一篇文章,作者提出 Python 其實也有編譯的過程,解釋器會先編譯再執行 不但如此,作者還認 ...


哈嘍大家好,我是鹹魚

不知道有沒有小伙伴跟我一樣,剛開始學習 Python 的時候都聽說過 Python 是一種解釋型語言,因為它在運行的時候會逐行解釋並執行,而 C++ 這種是編譯型語言

不過我今天看到了一篇文章,作者提出 Python 其實也有編譯的過程,解釋器會先編譯再執行

不但如此,作者還認為【解釋】與【編譯】是錯誤的二分法、限制了編程語言的可能性。Python 既是解釋型語言,也是編譯型語言!

本文文字乾貨較多,耐心看完相信你會有不小的收穫

原文:https://eddieantonio.ca/blog/2023/10/25/python-is-a-compiled-language/

前言

本文所說的 Python ,不是指 PyPy、Mypyc、Numba、Cinder 等 Python 的替代版本,也不是像 Cython、Codon、mojo1這樣的類 Python 編程語言

我指的是常規的 Python——CPython

目前,我正在編寫一份教材,教學生如何閱讀和理解程式報錯信息(programming error messages)。我們正在為三種編程語言(C、Python、Java)開設課程

程式報錯信息的本質的關鍵點之一在於程式報錯是在不同階段生成的,有些是在編譯時生成,有些是在運行時生成

第一門課是針對 C 語言的,具體來說是如何使用 GCC 編譯器,以及演示 GCC 如何將代碼轉換成可執行程式

  • 預處理(preprocessing)
  • 辭彙分析(lexical analysis)
  • 語法分析(syntactic analysis)
  • 語義分析(semantic analysis)
  • 鏈接(linking)

除此之外,這節課還討論了在上述階段可能出現的程式報錯,以及這些報錯將如何影響所呈現的錯誤消息。重要的是:早期階段的錯誤將阻止在後期階段檢測到錯誤(也就是說 A 階段的報錯出現之後,B 階段就算有錯誤也不會檢測出來)

當我將這門課調整成針對 Java 和 Python 時,我發現 Python 和 Java 都沒有預處理器(preprocessor),並且 Python 和 Java 的鏈接(linking)不是同一個概念

我忽略了上面這些變化,但是我偶然發現了一個有趣的現象:

編譯器在各個階段會生成報錯信息,而且編譯器通常會在繼續執行之前把前面階段的報錯顯示出來,這就意味著我們可以通過在程式中故意創建錯誤來發現編譯器的各個階段

所以讓我們玩一個小游戲來發現 Python 解釋器的各個階段

Which Is The First Error

我們將創建一個包含多個 bug 的 Python 程式,每個 bug 都試圖引發不同類型的報錯信息

我們知道常規的 Python 每次運行只會報告一個錯誤,所以這個游戲就是——哪條報錯會被首先觸發

# 下麵是有 bug 的程式
1 / 0
print() = None
if False
    ñ = "hello

每行代碼都會產生不同的報錯:

  • 1 / 0 將生成 ZeroDivisionError: division by zero
  • print() = None 將生成 SyntaxError: cannot assign to function call
  • if False 將生成 SyntaxError: expected ':' .
  • ñ = "hello 將生成 SyntaxError: EOL while scanning string literal .

問題在於,哪個錯誤會先被顯示出來?需要註意的是:Python 版本很重要(比我想象的要重要),所以如果你看到不同的結果,請記住這一點

PS:下麵運行代碼所使用的 Python 版本為 Python 3.12

在開始執行代碼之前,先想想【解釋】語言和【編譯】語言對你來說意味著什麼?

下麵我將給出一段蘇格拉底式的對話,希望你能反思一下其中的區別

蘇格拉底:編譯語言是指代碼在運行之前首先通過編譯器的語言。一個例子是 C 編程語言。要運行 C 代碼,首先必須運行像 or clang 這樣的 gcc 編譯器,然後才能運行代碼。編譯後的語言被轉換為機器代碼,即 CPU 可以理解的 1 和 0

柏拉圖:等等,Java不是一種編譯語言嗎?

蘇格拉底:是的,Java是一種編譯語言。

柏拉圖:但是常規 Java編譯器的輸出不是一個 .class 文件。那是位元組碼,不是嗎?

蘇格拉底:沒錯。位元組碼不是機器碼,但 Java 仍然是一種編譯語言。這是因為編譯器可以捕獲許多問題,因此您需要在程式開始運行之前更正錯誤。

柏拉圖:解釋型語言呢?

蘇格拉底:解釋型語言是依賴於一個單獨的程式(恰如其分地稱為解釋器)來實際運行代碼的語言。解釋型語言不需要程式員先運行編譯器。因此,在程式運行時,您犯的任何錯誤都會被捕獲。Python 是一種解釋型語言,沒有單獨的編譯器,您犯的所有錯誤都會在運行時捕獲。

柏拉圖:如果 Python不是一種編譯語言,那麼為什麼標準庫包含名為 py_compile and compileall 的模塊?

蘇格拉底:嗯,這些模塊只是將 Python轉換為位元組碼。他們不會將 Python 轉換為機器代碼,因此 Python 仍然是一種解釋型語言。

柏拉圖:那麼,Python和 Java都轉換為位元組碼了嗎?

蘇格拉底:對。

柏拉圖:那麼,為什麼Python是一種解釋型語言,而 Java卻是一種編譯型語言呢?

蘇格拉底:因為 Python 中的所有錯誤都是在運行時捕獲的。 (ps:請註意這句話)

  • 回合一

當我們執行上面那段有 bug 的程式時,將會收到下麵的錯誤

  File "/private/tmp/round_1.py", line 4
    ñ = "hello  # SyntaxError: EOL while scanning string literal
        ^
SyntaxError: unterminated string literal (detected at line 4)

檢測到的第一個報錯位於源碼的最後一行。可以看到:在運行第一行代碼之前,Python 必須讀取整個源碼文件

如果你腦子裡有一個關於【解釋型語言】的定義,其中包括”解釋型語言按順序讀取代碼,一次運行一行”,我希望你忘掉它

我還沒有深入研究 CPython 解釋器的源碼來驗證這一點,但我認為這是第一個檢測到的報錯的原因是 Python 3.12 所做的第一個步驟是掃描(scanning ),也稱為詞法分析

掃描器將整個文件轉換為一系列標記(token),然後繼續進行下一階段。

掃描器掃描到源碼最後一行的字元串字面值末尾少了個引號,它希望把整個字元串字面值轉換成一個 token ,但是沒有結束引號它就轉換不了

在 Python 3.12 中,掃描器首先運行,所以這也是為什麼第一個報錯是unterminated string literal

  • 回合二

我們把第四行的代碼的 bug 修複好,第 1 2 3 行仍有 bug

1 / 0
print() = None
if False
    ñ = "hello"

我們現在來執行代碼,看下哪個是第一個報錯

File "/private/tmp/round_2.py", line 2
    print() = None
    ^^^^^^^
SyntaxError: cannot assign to function call here. Maybe you meant '==' instead of '='?"

這次是第二行報錯!同樣,我沒有去查看 CPython 的源碼,但是我有理由確定掃描的下一階段是解析(parsing),也稱為語法分析

在運行代碼之前會先解析源碼,這意味著 Python 不會看到第一行的錯誤,而是在第二行報錯

我要指出我為這個小游戲而編寫的代碼是完全沒有意義的,並且對於如何修複 bug 也沒有正確的答案。我的目的純粹是估計編寫錯誤然後發現 python 解釋器現在處在哪個階段

我不知道 print() = None可能是什麼意思,所以我將通過將其替換為print(None)來解決這個問題,這也沒有意義,但至少它在語法上是正確的。

  • 回合三

我們把第二行的語法錯誤也修複了,但源碼還有另外兩個錯誤,其中一個也是語法錯誤

1 / 0
print(None)
if False
    ñ = "hello"

回想一下,語法錯誤在回合二的時候優先顯示了出來,在回合三還會一樣嗎

  File "/private/tmp/round_3.py", line 3
    if False
            ^
SyntaxError: expected ':'

沒錯!第三行的語法錯誤優先於第一行的錯誤

正如回合二一樣,Python 解釋器在運行代碼之前會先解析源碼,對其進行語法分析

這意味著 Python 不會看到第一行的錯誤,而是在第三行報錯

你可能想知道為什麼我在一個文件中插入了兩個 SyntaxError,難道一個還不夠表明我的觀點嗎?

這是因為 Python 版本的不同會導致結果的不同,如果你在 Python3.8 或者更早的版本去運行代碼,那麼結果如下

在 Python 3.8 中,第 2 輪報告的第一個錯誤消息位於第 3 行:

1 / 0
print() = None
if False
    ñ = "hello"
  
# 報錯
File "round_2.py", line 3
    if False
            ^
SyntaxError: invalid syntax

修複第三行的錯誤之後,Python 3.8 在第 2 行報告以下錯誤消息:

1 / 0
print() = None
if False:
    ñ = "hello"

# 報錯
  File "round_3.py", line 2
    print() = None
    ^
SyntaxError: cannot assign to function call

為什麼 Python 3.8 和 3.12 報錯順序不一樣?是因為 Python 3.9 引入了一個新的解析器。這個解析器比以前的 naïve 解析器功能更強大

舊的解析器無法提前查看多個 token,這意味著舊解析器在技術上可以接受語法無效的 Python 程式

尤其是這種限制導致解析器無法識別賦值語句的左邊是否為有效的賦值目標,好比下麵這段代碼,舊解析器能夠接收下麵的代碼

[x for x in y] = [1,2,3]

上面這段代碼沒有任何意義,甚至 Python 語法是不允許這麼使用的。為瞭解決這個問題,Python 曾經存在過一個獨立的,hacky 的階段(這個 hacky 我不知道用什麼翻譯比較好)

Python會檢查所有的賦值語句,並確保賦值號左邊實際上是可以被賦值的東西

而這個階段是發生在解析之後,這也就是為什麼舊版本 Python 中會先把第二行的報錯先顯示出來

  • 回合四

現在還剩最後一個錯誤了

1 / 0
print(None)
if False:
    ñ = "hello"

我們來運行一下

Traceback (most recent call last):
  File "/private/tmp/round_4.py", line 1, in <module>
    1 / 0
    ~~^~~
ZeroDivisionError: division by zero

需要註意的是,Traceback (most recent call last)表示 Python 運行時報錯的主要內容,這裡在回合四才出現

經過前面的掃描、解析階段,Python 終於能夠運行代碼了。但是當 Python 開始運行解釋第一行的時候,引發一個名為 ZeroDivisionError 的報錯

為什麼知道現在處於【運行時】,因為 Python 已經列印出 Traceback (most recent call last),這表示我們有一個堆棧跟蹤

堆棧跟蹤只能在運行時存在,這意味著這個報錯必須在運行時捕獲。

但這意味著在回合1~3 中遇到的報錯不是運行時報錯,那它們是什麼報錯?

Python 既是一種編譯語言,也是一種解釋語言

沒錯!CPython 解釋器實際上是一個解釋器,但它也是一個編譯器

我希望上面的練習已經說明瞭 Python 在運行第一行代碼之前必須經過幾個階段:

  • 掃描(scanning )
  • 解析(parsing )

舊版本的 Python 多了一個額外階段:

  • 掃描(scanning )
  • 解析(parsing )
  • 檢查有效的分配目標(checking for valid assignment targets)

讓我們將其與前面編譯 C 程式的階段進行比較:

  • 預處理
  • 辭彙分析(“掃描”的另一個術語)
  • 語法分析(“解析”的另一個術語)
  • 語義分析
  • 鏈接

Python 在運行任何代碼之前仍然執行一些編譯階段,就像 Java一樣,它會把源碼編譯成位元組碼

前面三個報錯是 Python 在編譯階段產生的,只有最後一個才是在運行時產生,即ZeroDivisionError: division by zero.

實際上,我們可以使用命令行上的 compileall 模塊預先編譯所有 Python 代碼:

$ python3 -m compileall

這會將當前目錄中所有 Python 文件的編譯位元組碼放入其中 __pycache__/ ,並顯示任何編譯器錯誤

如果你想知道那個 __pycache__/ 文件夾中到底有什麼,我為 EdmontonPy 做了一個演講,你應該看看!

演講地址:https://www.youtube.com/watch?v=5yqUTJuFuUk&t=7m11s

只有在 Python 被編譯為位元組碼之後,解釋器才會真正啟動,我希望前面的練習已經證明 Python 確實可以在運行時之前報錯

編譯語言與解釋語言是錯誤的二分法

每當一種編程語言被歸類為【編譯】或【解釋】語言時,我都會感到很討厭。一種語言本身不是編譯或解釋的

一種語言是編譯還是解釋(或兩者兼而有之!)是一個實現細節

我不是唯一一個有這種想法的人。Laurie Tratt 有一篇精彩的文章,通過編寫一個逐漸成為優化編譯器的解釋器來論證這一點

文章地址:https://tratt.net/laurie/blog/2023/compiled_and_interpreted_languages_two_ways_of_saying_tomato.html

還有一篇文章就是 Bob Nystrom 的 Crafting Interpreters。以下是第 2 章的一些引述:

編譯器和解釋器有什麼區別?

事實證明,這就像問水果和蔬菜之間的區別一樣。這似乎是一個二元的非此即彼的選擇,但實際上“水果”是一個植物學術語,而“蔬菜”是烹飪學術語。

嚴格來說,一個並不意味著對另一個的否定。有些水果不是蔬菜(蘋果),有些蔬菜不是水果(胡蘿蔔),但也有既是水果又是蔬菜的可食用植物,如西紅柿

當你使用 CPython 來運行 Python 程式時,源碼會被解析並轉換成內部位元組碼格式,然後在虛擬機中執行

從用戶的角度來看,這顯然是一個解釋器(因為它們從源碼運行程式),但如果你仔細觀察 CPython(Python 也可譯作蟒蛇)的鱗狀表皮(scaly skin),你會發現它肯定在進行編譯

答案是:CPython 是一個解釋器,它有一個編譯器

那麼為什麼這很重要呢?為什麼在【編譯】和【解釋】語言之間做出嚴格的區分會適得其反?

【編譯】與【解釋】限制了我們認為編程語言的可能性

編程語言不必由它是編譯還是解釋來定義的!以這種僵化的方式思考限制了我們認為給定的編程語言可以做的事情

例如,JavaScript 通常被歸入“解釋型語言”類別。但有一段時間,在 Google Chrome 中運行的 JavaScript 永遠不會被解釋——相反,JavaScript 被直接編譯為機器代碼!因此,JavaScript 可以跟上 C++ 的步伐

出於這個原因,我真的厭倦了那些說解釋型語言必然慢的論點——性能是多方面的,並且不僅僅取決於"預設"編程語言的實現

JavaScript 現在很快了、Ruby 現在很快了、Lua 已經快了一段時間了

那對於通常被標記為編譯型語言的編程語言呢?(例如 C)你是不會去想著解釋 C 語言程式的

語言之間真正的區別

語言之間真正的區別:【靜態】還是【動態】

我們應該教給學生的真正區別是語言特性的區別,前者可以靜態地確定,即只盯著代碼而不運行代碼,後者只能在運行時動態地知道

需要註意的是,我說的是【語言特性】而不是【語言】,每種編程語言都選擇自己的一組屬性,這些屬性可以靜態地或動態地確定,並結合在一起,這使得語言更“動態”或更“靜態”

靜態與動態是一個範圍,Python 位於範圍中更動態的一端。像 Java 這樣的語言比 Python 有更多的靜態特性,但即使是 Java 也包括反射之類的東西,這無疑是一種動態特性

我發現動態與靜態經常被混為一談,編譯與解釋混為一談,這是可以理解的

因為通常使用解釋器的語言具有更多的動態特性,如 Python、Ruby 和 JavaScript

具有更多靜態特性的語言往往在沒有解釋器的情況下實現,例如 C++ 和 Rust

然後是介於兩者之間的 Java

Python 中的靜態類型註釋已經逐漸(呵呵)在代碼庫中得到採用,其中一個期望是:由於更多靜態的東西,這可以解鎖 Python 代碼中的性能優勢

不幸的是,事實證明,Python 中的類型(是的,只是一般類型,考慮元類)和註釋本身都是Python 的動態特性,這使得靜態類型不是大伙所期望的性能優勢

最後總結一下:

  • CPython 是一個解釋器,它有一個編譯器(或者說 Python 既是解釋型語言,也是編譯型語言)
  • Python 是編譯的還是解釋的並不重要。重要的是,相對於那些具有更多靜態屬性(在編譯或解釋階段可以在運行前確定的屬性)的編程語言,Python 中可以在運行前確定的屬性相對較少,這意味著在 Python 中,許多屬性是在運行時動態確定的,而不是在編譯或解釋時靜態確定的
  • 由於 Python 具有較少的靜態屬性,這意味著在運行時,某些錯誤可能只能在運行時才會顯現,而不是在編譯或解釋時就能被髮現
  • 因為在具有更多靜態屬性的編程語言中,許多錯誤會在編譯或解釋時被捕獲,因此更容易在編碼階段就發現和修複
  • 這是真正重要的區別,這是一個比【編譯】和【解釋】更細緻、更微妙的區別。出於這個原因,我認為強調特定的靜態和動態特性是很重要的,而不是一昧的局限於“解釋型”和“編譯型”語言之間的繁瑣的區別。

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 提起 jackson,在日常使用中,由於涉及到各種序列化和反序列化的處理,就不能不提 註解,瞭解註解的常用方式可以極大地方便我們處理序列化,今天分享一些在使用 jackson 中涉及到的註解。 目錄1.@JsonProperty - 欄位命名2.@JsonPropertyOrder - 欄位序列化順 ...
  • 使用腳本進行下載的需求很常見,可以是常規文件、web頁面、Amazon S3和其他資源。Python 提供了很多模塊從 web 下載文件。下麵介紹 一、使用 requests requests 模塊是模仿網頁請求的形式從一個URL下載文件 示例代碼: import requests url = 'x ...
  • 字元串操作 1.字元串的翻轉 # 方式一 s = 'hello world' print(s[::-1) # 方式二 from functools import reduce print(reduce(lambda x,y:y+x, s)) 2.判斷字元串是否是迴文 利用字元串翻轉操作可以查看字元串 ...
  • 前言 由於相容性問題,使得我們若想用較新版本的 PyTorch,通過 GPU 方式訓練模型,也得更換較新版本得 CUDA 工具包。然而 CUDA 的版本又與電腦顯卡的驅動程式版本關聯,如果是低版本的顯卡驅動程式安裝 CUDA11 及以上肯定會失敗。 比如 GTX750Ti 或 GTX1050Ti,出 ...
  • 寫在前面 此異常非彼異常,標題所說的異常是業務上的異常。 最近做了一個需求,消防的設備巡檢,如果巡檢發現異常,通過手機端提交,後臺的實時監控頁面實時獲取到該設備的信息及位置,然後安排員工去處理。 因為需要服務端主動向客戶端發送消息,所以很容易的就想到了用WebSocket來實現這一功能。 WebSo ...
  • Volatile 保證可見性 private volatile static Integer num = 0; 使用了volatile關鍵字,即可保證它本身可被其他線程的工作記憶體感知,即變化時也會被同步變化。 不保證原子性 原子性:不可分割 線程A在執行任務時是不可被打擾的,也不能被分割,要麼同時成 ...
  • 原文:juejin.cn/post/7283798251403821056 本文筆者計劃從全局角度來對Mybatis的整體架構及進行一次回顧和總結,希望能幫助你更加透徹的理解Mybatis。 1、前言 MyBatis是一款ORM(Object-Relational Mapping)框架,其主要用於將 ...
  • 問題 運行Springboot測試類時,查詢資料庫裡面數據顯示如下白網頁 程式報如下錯誤 解決方案 Spring Boot應用未能啟動的原因是它沒有找到合適的資料庫配置具體來說,它需要一個數據源(DataSource),但未能在你的配置中找出,也沒有找到任何嵌入式資料庫(H2, HSQL 或 Der ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...