Python進階技巧 (5) — Python 到底怎麼被執行?直譯、編譯、字節碼、虛擬機看不懂?

Jack Cheng
整個程式都是我的咖啡館
19 min readMay 3, 2019

「深入淺出的一次把最基礎的 Python 底層流程說給你聽!」

難易度:★★★★☆

實用度:★★★☆☆

by pixabay.com

【導言】

如果有一個人說「我會 Python(或其他任意程式語言)。」那面試官可能就會針對幾個面向來測試你到底是不是真的「會」,還是只是「學過」。包含「該語言執行過程是什麼」、「該語言經典的幾個基本指令背後所採用的算法」、「對於相同任務下,採用什麼寫法效率最高,什麼寫法易讀性最好,什麼寫法可擴充性最好」等等,這些問題同時也是學習者可以不斷自我重新省思的指標。

今天就要來帶大家,簡單卻又儘量完整(自己說)的講解 Python 這門程式語言到底是如何被執行,當中也會簡單與其他程式語言做比較,讓讀者更能夠感受差異。

(先打一下預防針,由於每一個步驟都涉及非常龐大深層的技術,此文是為了幫助 Python 學習者更加流暢與完整的認識整體執行流程,所以不管是在名詞的採用或是技術講解的深度,都以能夠幫助理解為主就好。)

【本文章節】

  • 壹、Python 被執行的整體流程
  • 貳、Python 與 Interpreter
  • 參、Python 的 Byte Code
  • 肆、Python 與 Virtual Machine
  • 伍、Python 可以直接編譯成可執行檔嗎?
  • 陸、直譯型 or 編譯型語言?

【範例環境與建議先備知識】

OS Ubuntu 16.04

Python 3.7

Required Knowledge

  • 程式語言框架

【壹、Python 被執行的整體流程】

Python 執行流程 — 簡易版
Python 執行流程 — 補充版

開頭直接給大家看兩張總流程圖,第一張是簡化版本,第二張是對於我接下來文章所會提及的內容做標注(基本上一定要看完文章才能理解當中各個標注的含意),可以一邊看圖一邊服用本文。

解說前,先讓我們對於對於名詞做統一,網路上每篇文章所用的名詞常有些出入,導致跨文章閱讀時會出現很大的理解障礙,所以這裡跟大家確認利用這張我繪製的表做名詞上的共識。英文翻中文的部份,基本上沒有什麼問題,也儘量把網路上資料中可能出現的各種翻譯羅列出來,不過我真心建議名詞的部份最好全部看英文的(嘮叨)。

至於網路上最常出現混淆的名詞就是「譯」系列的名詞,包含「翻譯」、「直譯」、「解釋」、「轉譯」以及「編譯」。Interpret 對應到「翻譯」、「直譯」、「解釋」,compile 對應到 「編譯」,至於「轉譯」由於只有少數地方出現,視為作者自己發明的暫時不採用。不過也想強調,此處的定義只是為了提昇本文的書寫性與閱讀性,最重要的「每一步的 input 是什麼, output 是什麼」!看到其他文章用不一樣的名詞,自己心裡要轉換成圖像與概念~

好啦!嚴肅的澄清並定義完本篇文章出現的名詞後,開始快速帶過 Python 被「執行」的流程~!!!

一般而言,以 Python3 為例,我們要執行一個 .py 檔,會下以下指令

> python3 file.py

此時會發生:

1. Interpreter 會將這份 Python Source Code file.py interpret 成 Byte Code

2. 然後 interpret 完的 Byte Code 才會再被 Virtual Machine compile 成 Machine Code 以及 Executable Code 等等的

3. 最後,Virtual Machine 會觸發 CPU 以及一大堆系統調度來處理並完成我們想要這份程式碼完成的事(如果沒有 bug ^^)。

所以 Python 在整個被執行的過程,會產生「主要」兩大 Code,先 Byte Code,再 Machine Code (或 Executable Code)。與 C 語言被執行的過程不同,它只會直接產生一個「主要」的 Code,也就是直接到達 Machine Code。(會強調「主要」是因為中間還有許多超出本文範圍的細小過程,不過並不是本專欄會書寫的範疇)

相對的,C 語言本身不會轉成 Byte Code 而是直接轉成最後的 Machine Code ,隨後也不需要透過 Virtual Machine 就可以直接觸發 CPU 以及一大堆系統的調度處理並完成任務。

好了,到此處如果是第一次接觸的人,大概可以感受到兩者流程的差異,但可能不清楚為什麼要有這兩種流程,也可能對於文中提到的名詞陌生,以下各小節就來一一簡單解說!!!

【貳、Python 與 Interpreter】

Python 被執行的過程,第一步就是被 Interpreter 給 interpret 成 Byte Code。顧名思義,Byte Code 裡面就是儲存 Byte 型態的資料,而資料內容便是把原先我們熟悉的 Python Source Code 轉換成 Virtual Machine 想看到的檔案格式,所以有人又稱 Byte Code 是 Virtual Machine 的 Machine Code 。

那為什麼要設計這樣的流程去產生一個中間程式碼,而不要直直通到底呢?原因有很多,其中包含了:

  • 提供更靈活架構方便轉換、提昇嫁接性(例如 Byte Code 可以對接 Java 的 Virtual Machine)
  • 承上,進一步與 Virtual Machine 做結合,達成跨平台、系統的效果(這部份由於技術不斷蓬勃發展,很多後來都可以達到跨平台,所以比較屬於陳腔濫調的解釋)
  • 可以作到市面上常說的「一行一行執行」的概念,而不是花費許多資源做通到底的編譯,提昇開發效率

支援 Python 的 Interpreters 有哪些呢?常見的有

  • CPython (C 寫成)
  • PyPy (RPython 寫成)
  • Jython (Java 寫成)

其中又以 CPython 為最大宗 (PyPy 對於技術開發人員來說也滿常用的),基本上大家下載 Python 時其實都是下載 CPython 版本的 Interpreter。所以本篇文章如果提及 Interpreter 的內容,沒有特別註明的話都是以 CPython 為例(其他的我也沒用過)。不同的 Interpreter 差別除了其程式語言不同外,不同的 Interpreters 輸出的 Byte Code 也可能不同,CPython / PyPy 輸出的 Byte Code 需要 C Virtual Machine 來吃,Jypthon 輸出的 Byte Code 須要 JVM (Java Virtual Machine 來吃,各種搭配不外乎就是在效率、相容性、其他操作特色等等拼個高下。

那 CPython 到底是什麼呢?它就是 C 語言撰寫的 Interpreter,所以其實大家在執行 Python 時,都偷偷有在執行 C 語言的程式碼呢!

帶大家看一下 CPython 的一小段程式碼:

https://github.com/python/cpython/blob/master/Objects/listobject.c

這段就是人人都會用到的「List 取值」的主要程式碼,有沒有覺得很有趣 xDDD 我們平常寫的 Python Source Code 就是被這樣的 C 程式碼 interpret 成 ByteCode 的!

有興趣的人可以自己到 CPython 官方 Github 看看更多 CPython 程式碼,尤其是對於已經需要要求到 computation 效能的開發者來說,看 CPython 程式碼理解、分析背後的算法與效率潛在瓶頸甚至運用到程式設計上都是家常便飯的事了~

【參、Python 的 Byte Code】

和 Java 一樣,Python 被執行中間過程會有 Byte Codes 扮演過渡角色,是 Intermediate Code(中間程式碼)的一種。任何的 Code 基本上都只是「外皮」,重要的是它外皮下蘊含的資訊要被「什麼工具/軟體」來分析理解,而一般不同形式的 Byte Codes 會被丟入對應的 Virtual Machines 做分析進行下一步的處理,而我們平常大多默默使用 CPython + C Virtual Machine 這個組合。

值得一提的是,Byte Code 除了被 C Virtual Machine 讀取進行下一步操作外,Python 還會把可能常使用的 Byte Code 寫進 Disk 儲存起來,變成我可以看到的 .pyc 檔!沒錯,就是很多人常常執行完 Python 後,常會自動產生卻不知道是什麼檔案的 .pyc 檔!

.pyc 檔是 PyCodeObject,是 Byte Code 在 Python 裡的一個實現,有些人以為檔中的 ‘c’ 是 compile 檔的意思,但應該會是 PyCodeObject 的意思比較接近。

為什麼需要 .pyc 檔呢?主要的理由是加速運行,同樣的 Python Source Code 不需要再 interpret 成 Byte Code 一次,直接讀取就可以,所以通常每次運行 Python 時,它都會先檢查是否有已經生成過得 .pyc檔,並比對檢查修改的時間,確定沒問題後就直接讀取使用,可算是某種 cache 檔。此外,資料中還有看到一個原因,便是為了防止程式碼外漏,將 Python Source Code interpret 成 Byte Code,讓大多數人無法直接知道程式碼內容,然而,有一點涉獵的人都可以知道這樣是無法完全防止的,可以透過反編譯重新獲取 Python Source Code,此外 .pyc 檔對於相同的 Python Source Code 但在不同版本的 Python 下所產生的 Byte Code 基本上是不同的,也增加反編譯的難度,接下來的編譯反編譯攻防戰就是題外話了,Let’s hold it back.

什麼樣的情況下會產生 .pyc 檔呢?Python 預設情形是會對 module 的 .py 檔所產生的 Byte Code 儲存成 .pyc 檔,簡而言,就是被你主程式 import 的那些 .py 檔都會這樣做。但你也大可以自己手動對於任何 .py 檔都生成 .pyc 檔,可以參考 DK’s Blog - python下编译py成pyc和pyo

Python 自己預設產生的 .pyc 檔會存在哪裡呢?使用 Python3 的話,會存在 __pycache__/ 這個資料夾當中,也是很多人常常執行完 Python 後會看到卻可能不清楚是什麼的東東!此時可能會有人問,「那平時 import 一大堆 module,像是 numpy 、torch、requests 等等,為什麼都沒有看到相應的 .pyc 檔?」(筆者真的很愛自問自答,還硬要說有人問),其實都是有產生.pyc 檔的,只是存在你安裝該 module/library 的環境路徑底下,以筆者為例,就是儲存在我 /user/local/lib/python3.7/dist-packages 的各個 module/library 底下,都會有 __pycache__/ 。此外值得一提的是如果是自己 import 自己寫的 code,如果在使用 pipenv 的環境下(什麼是 pipenv? 可以閱讀 Python 工具箱 (1) — 專案必備 Pipenv對,甚至業配自己文章),筆者實驗發現會無法產生相應的 module .pyc 檔。

最後帶大家來看一下,把 Byte Code .pyc 檔打開來大概長怎麼樣,原先的 Python Source Code 如下:

上半部為主程式,下半部為被 import 的 module,是不同的兩份 .py 檔,此處僅為了呈現方便。

執行後,產在 __pycache__/ 資料下,裡面會生成 sample_code.cpython-37.cpy 檔,點開後顯示:

B�������\�������������������@���s ���d�Z�dZdS�)i����d���N)�xZdog��r���r����4/media/jack/File/pythonProjects/notes/sample_code.py�<module>���s���

好,史上最沒有意義的展示結束了,既然是 Byte 為主的內容我們當然看不懂啊^^(好啦,裡面還是有像 dog 以及文件路徑可以辨識出。)

最後,帶大家利用一個套件來查看 Byte Code 裡頭到底包含什麼樣的資訊 —— dis 套件!!!

以下是一段讓狗叫三下的 Python Source Code:

使用 dis 套件,並把想要分析的 function 丟到 dis.dis() 裡面,他就會自動顯示這段 Python Source Code 在 Byte Code 所對應的「資訊」是什麼,輸出如下:

4           0 SETUP_LOOP              24 (to 26)
2 LOAD_GLOBAL 0 (range)
4 LOAD_CONST 1 (3)
6 CALL_FUNCTION 1
8 GET_ITER
>> 10 FOR_ITER 12 (to 24)
12 STORE_FAST 0 (i)
5 14 LOAD_GLOBAL 1 (print)
16 LOAD_CONST 2 ('Miouwo ~~~')
18 CALL_FUNCTION 1
20 POP_TOP
22 JUMP_ABSOLUTE 10
>> 24 POP_BLOCK
>> 26 LOAD_CONST 0 (None)
28 RETURN_VALUE

此時顯示的內容,就可以視為 CPython 轉換出的 Byte Code 所蘊含的資訊!可以看出簡單的 Python Source Code 背後是被轉換成較為複雜的一個一個步驟(講廢話),至於 Byte Code 的架構說明可以參考閱讀 TechBridge 技術共筆部落格 以及 Python 底層運作 01 — 虛擬機器與 Byte Code 裡的介紹。

【肆、Python 與 Virtual Machine】

大致了解完 Byte Code 後,就要來稍微知道解析 Byte Code 的 Virtual Machine 了。

Virtual Machine 基本上可以在任何系統上讀取並解析 Byte Code,所以任何系統上安裝對應正確的 Virtual Machine 後都可以執行相同的 Byte Code,達到所謂的跨平台、系統,這一點和 Machine Code 、Object Code 和 Executable Code 不同,它們必須根據不同系統而有不一樣的 Code,所以同一份 Executable Code 在 Windows 上可以執行卻不能在 Linux 上執行,但同一份 Byte Code 就可以在 Windows 和 Linux 上被執行(前提是安裝的 Python 版本一樣喔)。

常見支援 Python 的 Virtual Machine 有

  • C Virtual Machine
  • JVM (Java Virtual Machine)

這邊對於名詞 Virtual Machine 補充一下,根據 Wikipedia 的分類,將 Virtual Machine 分為兩大類,以及一個較新但不歸在前述兩大類中的類別

  • System Virtual Machine (系統虛擬機)
  • Process Virtual Machine (程式虛擬機)
  • Operating-system-level Virtualization (作業系統層虛擬化)

System Virtual Machine 包含了我們常用的產品 Virtualbox 以及 VMWare 等等,大家應該不陌生。Process Virtual Machine 的部份,我們通篇文章所提到的 Virtual Machine 都屬此類,最經典例子是 JVM,其中的運作就是根據 Byte Code 去操作 CPU 、 Register 等等,最後完成執行任務,達成我們想要的結果。Operating-system-level Virtualization 並不真正屬於任何一類,但較為接近 System Virtual Machine,只是更輕巧且處理系統的部份更少一些,而其應用我想大家也不陌生, 每天包來包去的 Docker 就是屬於這一類。

一般來說,我們的 Byte Code 被 Process Virtual Machine 吃進去後,最後經過一堆調度,變成 Object Code 或是 Executable Code (Binary Code),然後再經過一些系統以及物理機器上的調度,讓物理機器真正的去執行該完成的任務。對於大部分的人來說,Executable (Binary File) 這個階段的內容已經可以視為最低階、最接近底層的了,可以簡單視為「最後的產物」,Executable Code 在 Windows 系統上若儲存在 disk 上為.exe檔,在 linux 系統上因沒有副檔名的限制所以不一定,一般來說 .bin 檔有很大的機會是,另外 Java 產生的 .jar.class檔以及 C 產生的.out 檔也是「比較」接近 Executable Code 的 ( 精確說,是更接近 Object Code 的,但此處為了類比方便,而做此解釋)。經過 Process Virtual Machine 處理用並完成執行任務,我們可以說完成了一次 Python 的執行,結束了!

在這裡解說 Virtual Machine 是為了澄清本文的 Virtual Machine 到底所指為何,並沒有要繼續解說所有 Virtual Machine 的運作原理。其他更詳細有關 Virtual Machine 的運作原理可以參考閱讀 Python 字节码介绍 中對於 Virtual Machine 的內容。甚至,如果讀者從來都沒有接觸過其他 Virtual Machine 的話,那細節的部份可以稍微看過去就好!

此外,電資或相關背景或的人可能會覺得這裡說的不夠仔細,例如「區分 Object Code 和 Executable Code」、「Linker 怎麼沒有解說到」、「Binary File 與 Executable 的差異」等等,這邊全都簡化成較為合理的流程解說,其他的細節一方面教科書與資料常常出現歧義與解釋模糊,另一方面是真的超出本文範疇,所以不再贅述 QQ

最後補充一點,有些資料會把我採用定義中的 ’Interpreter’ + ‘virtual machine’ 合併叫做 ‘Interpreter’,也就是我在 Python 執行流程 — 補充版 途中用棕黃色的虛線框起來的地方統一合併起來,所以對於這一類的資料而言,Interpreter 這個名詞不僅把 Python Source Code 轉成了 Byte Code,還同時一路完成到最後的 Executable Code 並執行完成任務。流程是一樣的,只是它的區分名詞方式不同,供大家參考~

【伍、Python 可以直接編譯成可執行檔嗎?】

如果按照本文所寫的流程,我們每次執行 Python 都需要跑一次完整流程,熟悉其他語言的讀者一定很急切想知道,Python 能不能支援直接編譯成 Object Code 或是 Executable Code 這種層級的檔案(例如 .bin .exe),把握一個原則,高階的 Code 在邏輯上幾乎都是可以轉換成低階的 Code,只要有人願意寫轉換的工具(對應的 Compiler)就可以,所以 Python 當然也可以直接打包成像是.exe 檔或是 linux 上的 Executable Files 囉!!

最常見的工具就是 PyPI 上的 PyInstaller 套件,可以參考 PyPI 上的說明 ,相當直白簡單,支援在 Window 、Linux 以及 MacOS 上轉換成 Executable Files 喔 (Windows 上就是輸出 .exe 檔)!其他還有像是 pytidylib 套件也可以使用!

【陸、直譯型 or 編譯型語言?】

初學 Python 最容易被問到或是被教到 Interpreted Language (直譯型語言) 與 Compiled Language (編譯型語言) 的區別,以及 Python 到底是哪一種語言,這兩大問題。

先強調一下,以下的解說是非常「主觀的」,不管書上或是網路上的資料也是眾說紛紜,大家如果有足夠的背景知識能夠判斷更好,如果目前尚未有或是有意聽聽,也可以稍微看一下我的個人看法!

結論是:

  1. 我覺得 Interpreted Language 與 Compiled Language 的區別在現代已經沒有太大意義,與其硬要把不斷增加的程式語言硬切成兩種分類,不如好好理解每個程式語言背後的執行流程與中間到底產生哪些東西。
  2. Python 「基本上」比較接近「先直譯」再「編譯」,也就是某些資料中所寫的「混合式」。但但但,但是,但是,前提是,你所使用的「直譯」和「編譯」這兩個詞是和我本文中所定義的一樣!另外,由於 Interpreted Language 與 Compiled Language 兩個分類根本就沒有正式統一的定義,如果根據 Wikipedia 所作的模糊定義來說,硬要說是 Interpreted Language 是合理的,也是教課書上通常出現的答案!

首先,一個名詞如果沒有被嚴謹或正式統一定義,去「分析」怎麼分類是沒有意義的,但是,去「分類」就可能是有意義的。也就是說,儘管名詞沒有嚴謹統一,如果大家都說 Python 是 Interpreted Language,那它被「分類」在 Interpreted Language 又有什麼錯呢?「分類」的目的包含了「溝通」,如果類似的新架構出現了,大家遵循 Python 是 Interpreted Language 的潛規則,並把新架構分類在 Interpreted Language,大家能夠快速比擬,簡單的理解該新架構更接近哪種程式語言,這樣也是達到了「溝通」的目的,但站在科研或是更嚴謹的角度來說,目的就不僅僅是「溝通」這個目的,也就沒有良好討論的基礎了!

由於越來越多的程式語言被開發,各種技術不斷改良與混雜組合,原先傳統的分類本來就很可能不堪負荷,所以才會有 Java 是採取 Interpreted 和 Compiled 混合的「混合式」一說。那根據 Python 的執行流程,甚至 Python 可以在最後對接 JVM 來說,它被歸類在「混合式」感覺也沒有錯吧!?甚至在看看現在已經可以直接將 Python Source Code 轉成 Executable File,對於分類如果沒有給予更強烈的定義,所有高階語言幾乎什麼樣的流程在理論上都可以大致互通。

【結語】

本文盡可能統整了充足的資料,並加以統整、定義與簡化,肯定會和許多資料無法 100% 吻合,讀者如果深厚的背景,相信可以自己判斷內容的含意,如果是初學者或是沒有相關背景知識的話,不妨先確實吸收整篇文章的脈落,再一一比對其他資料或者過往的學習經驗,應該在整體所要表達的內容上不會相差太遠才是!

此外,有些人或許會問,知道這個要做什麼?我個人覺得有很多原因,其中一個較為客觀的原因是——效能分析,尤其是在對 CPython 以及 Byte Code 所蘊含的操作邏輯與算法,開發者可以檢查某段程式碼的效能大多消耗在哪裡,記憶體如何被分配的,以及如何優化或是改用其他方式等等,但並不是每個人都會接觸到這樣的效能優化問題,所以只要明白整體流程我想大概就足夠了!

如果你也喜歡我們的文章,幫我們動動手部肌肉,按下掌聲Clap,讓我們有動力繼續煮下一頓料理!

--

--

Jack Cheng
整個程式都是我的咖啡館

Interested in ML, algorithms, and back-end. Studied M.S. at NTU GICE.