一、為什麼要學習 Python 原始碼
Python 是一門上層語言,建立者通過有意設計來隱藏背後複雜的細節 (builtins)。在解決專案問題時,很多問題也許能通過搜索引擎找到答案,但 Python 是一門迭代速度非常快的語言,搜索引擎與專業書難以獲得實效性好且準確的答案,因此多瞭解其架構與核心原理,可以更好地理解Python語言的使用方式、提高程式設計技能和除錯能力。
二、CPython 整體架構
CPython 整體架構大致分為三個模組:
程式碼檔案 File Groups - Python 所提供的的大量的模組、庫、以及使用者自定義的模組。使用者還可以通過自定義模組來擴充套件 Python 系統。
直譯器 Python Core - 又稱 Python 虛擬機器,對程式碼分析理解,翻譯成位元組流,並執行這些位元組程式碼。
· Scanner 負責詞法分析的工作,將程式碼一行一行切分為 Token
· Parser 則負責語法分析,將 Token 組織為抽象語法樹
· Compiler 則將語法樹轉化為指令集合的位元組碼流
· Code Evaluator 也是我們常說 Python 虛擬機器,負責執行這些位元組碼
執行環境 Runtime Env - 包括執行時的物件、基礎型別結構、記憶體分配器和實時的執行狀態資訊。
· Object 和 Type Structure 分別是程式在執行過程中生成的物件和Python中的自帶內建物件,如 Int、Str、List 等
· Memory Allocator 則負責申請建立物件需要的記憶體,本質就是封裝了 C 語言裏面的 malloc() 函式
· Current State 負責維護執行時的各類狀態資訊,以便在程式執行過程中如果發生狀態變化(正常態和異常態)時,仍然能正常執行
三、編譯 CPython
我們可以從下文的 GitHub 地址下載各版本的 CPython 原始碼(本文內容以 Python 3.11 為例),其目錄結構如下:
接下來,我們將從原始碼編譯 CPython。此步驟需要 C 編譯器和一些構建工具。不同的系統編譯方法也不同,這裏我用的是 mac 系統。
在上述命令中,你需要下載並安裝一些工具,包括 Homebrew,Git,Make, GNU C 編譯器和OpenSSL等。./configure步驟用來自動化構建過程,CPPFLAGS 是 c 和 c++ 編譯器的選項,這裏指定了 zlib 標頭檔案的位置,LDFLAGS 是 gcc 等編譯器會用到的一些優化引數,這裏是指定了 zlib 庫檔案的位置,(brew --prefix openssl) 顯示的是 openssl 的安裝路徑,執行完上面命令以後在儲存庫的根目錄中會生成一個 Makefile,你可以通過執行以下命令來構建 CPython 二進制檔案。make -j2-j2 標誌允許 make 同時執行 2 個作業來加快編譯速度。在構建期間,你可能會收到一些錯誤,例如,dbm,sqlite3,uuid,nis,ossaudiodev,spwd 和tkinter 將無法使用這組指令構建。如果你不打算針對這些軟體包進行開發,這些錯誤沒什麼影響。構建將花費幾分鐘並生成一個名為 python.exe 的二進制檔案,雖然它的字尾是 exe 格式,但它確實是 macOS 下的可執行檔案。每次改動原始碼,都需要重新執行 make 進行編譯。
四、瞭解 Python 物件
(一)PyObject 和 PyVarObject
Python 中一切皆物件,而所有的物件都擁有一些共同的資訊(也叫頭部資訊),這些資訊就在 PyObject 中,PyObject 是 Python 整個物件機制的核心,是 CPython 物件構造器的基石,我們來看看它的定義:
因此我們看到 PyObject 的定義非常簡單,就是一個引用計數和一個型別指標,所以 Python 中的任意物件都必有引用計數和型別這兩個屬性。針對變長物件,Python 底層也提供了一個結構體,因為 Python 裏面很多都是變長物件。我們來看看 PyVarObject 的定義:
例如列表(PyListObject 例項)中的 ob_size 維護的就是列表的元素個數,插入一個元素,ob_size 會加1,刪除一個元素,ob_size 會減1。因此,我們使用 len 獲取列表的元素個數是一個時間複雜度為 O(1) 的操作,因為 ob_size 始終和內部的元素個數保持一致,使用 len 獲取元素個數的時候會直接訪問 ob_size。
(二)PyTypeObject 型別物件
而將一個物件和其型別物件關聯起來的,毫無疑問正是該物件內部的 PyObject 中的 ob_type,也就是型別指標。我們通過物件的 ob_type 成員即可獲取型別物件的指標,通過該指標可以獲取儲存在型別物件中的某些元資訊。我們來看看 _typeobject 的幾個關鍵的成員:
事實上從名字上你也能看出來這每一個成員代表的含義,與我們在 Python 中常用的魔法方法很像。而且這裏面的成員雖然多,但並非每一個型別物件都具備,比如 int 型別它就沒有 tp_as_sequence 和 tp_as_mapping,所以 int 型別的這兩個成員的值都是 0。綜上所述,Python 底層通過 PyObject 和 PyTypeObject 完成了 C++ 所提供的物件的多型特性。在 Python 中建立一個物件,會分配記憶體並進行初始化,然後 Python 會用一個 PyObject * 來儲存和維護這個物件,因此在 Python 中,變數的傳遞(包括函式的引數傳遞)實際上傳遞的都是一個泛型指標:PyObject *。這個指標具體指向什麼型別的物件我們並不知道,只能通過其內部的 ob_type 成員進行動態判斷,而正是因為這個 ob_type,Python 實現了多型機制。以變數 a + b 為例,這個 a 和 b 指向的物件可以是整數、浮點數、字串、列表、元組、甚至是我們自己實現了 add 方法的類的例項物件。因為我們說 Python 中的變數都是一個 PyObject *,所以它可以指向任意的物件,因此 Python 就無法做基於型別方面的優化。首先 Python 底層要通過 ob_type 判斷變數指向的物件到底是什麼型別,這在 C 的層面上至少需要一次屬性查詢。然後 Python 將每一個操作都抽象成了一個魔法方法,所以例項相加時要在型別物件中找到該方法對應的函式指標,這又是一次屬性查詢。找到了之後將 a、b 作為引數傳遞進去,這會發生一次函式呼叫,會將物件維護的值拿出來進行運算,然後根據相加的結果建立一個新的物件,再返回其對應的 PyObject * 指標。而對於 C 來講,由於已經規定好了型別,所以 a + b 在編譯之後就是一條簡單的機器指令,因此兩者在效率上差別很大。
(三)物件的建立與呼叫
丟擲個問題: item = 2.71 和 item = float(2.71) 得到的結果都是2.71,但它們之間有什麼不同呢。或者說列表: lst = [] 和 lst = list()得到的 lst 也都是一個空列表,但這兩種方式有什麼區別呢?Python 中有許多效果相同,過程不同的表達,值得我們進一步思考。
事實上,Python 內部建立一個物件的方法有兩種:
• 通過 Python/C API,可以是泛型API、也可以是特型API,用於內建型別
• 通過對應的型別物件去建立,多用於自定義型別Python 對外提供了 C API,讓使用者可以從 C 環境中與其互動。由於 Python 直譯器是用 C 寫成的,所以 Python 內部也在大量使用這些 C API。爲了更好的研讀原始碼,系統地瞭解這些 API 的組成結構是很有必要的,下面以 PyFloatObject 物件為例,通過原始碼的大致步驟瞭解它的兩種建立過程。首先先看浮點數的定義:
可以看出,PyFloatObject 的結構非常簡單,除了 PyObject 這個公共的頭部資訊之外,只有一個額外的 ob_fval,用於儲存具體的值,並且使用的是 C 中的 double。以 f = 3.14 為例,底層結構如下:
使用泛型 API 建立
使用特型 API 建立
綜上,不管採用哪種方式建立,最終的關鍵步驟都是分配記憶體,建立內建型別的例項物件,Python 是可以直接分配記憶體的。因為它們有哪些成員在底層都是寫死的,Python 對它們瞭如指掌,因此可以通過 Python/C API 直接分配記憶體並初始化。以 PyFloat_FromDouble 為例,直接在介面內部為 PyFloatObject 結構體例項分配記憶體,並初始化相關欄位即可。從下文的實驗也可以看出,對於內建型別的例項物件而言,使用 Python / C API 建立要快不少。
比如建立列表:可以使用 list()、也可以使用 [ ];建立元組:可以使用 tuple()、也可以使用 ();建立字典:可以使用 dict()、也可以使用 {}。前者是通過型別物件去建立的,後者是通過 Python/C API 建立。但對於內建型別而言,我們推薦使用 Python/C API 建立,會直接解析為對應的 C 一級數據結構,因為這些結構在底層都是已經實現好了的,是可以直接用的,無需通過諸如 list() 這種呼叫型別物件的方式來建立,因為它們內部還是使用了 Python/C API。
五、總結
Python是一門備受推崇的指令碼語言,以其簡單的語法和全面的功能而著稱,可快速實現各種業務。本文從 CPython 物件構造器入手,介紹了浮點數物件在 CPython 底層數據結構中的表現形式以及物件建立的過程。通過進一步瞭解 CPython 動態性的實現方式,讀者可望在閱讀 CPython 原始碼後提升編寫高質量程式碼的能力。參考資料:
- https://github.com/python/cpython
- https://docs.python.org/zh-cn/3.11/c-api/index.html
- https://jiuaidu.com/jianzhan/990904/
- https://www.ab62.cn/article/15965.html
- https://zhuanlan.zhihu.com/p/596637636