來讀 CPython 原始碼
什麼是 CPython?
如果這是你第一次聽到 CPython 這個名字,也許你會以為這又是一款新的 Python 實作品。不不不,CPython 不是什麼新玩具,它就是大家平常在用的那個 Python 本尊。因為 Python 是使用 C 語言開發的,所以通常講到它的時候,沒特別聲明的話指的就是 CPython。
為什麼要讀原始碼?
為什麼喔?沒有為什麼,就好玩而已,好玩很重要!
看到這裡,如果各位預期透過閱讀 CPython 原始碼讓你的撰寫 Python 的功力大增,那很可能要失望了。閱讀 CPython 原始碼的確會增加 Python 的功力,但不多。CPython 的原始碼大部份都是 C 語言寫的,所以硬要說的話,閱讀 CPython 原始碼比較能增進你的 C 語言的功力。
所以,如果你只是想要學習 Python 語法的話,只要看「為你自己學 Python」的入門應用篇就很足夠了。閱讀 CPython 的原始碼, 最主要是可以了解 Python 背後的黑魔法,例如大家常常在講的物件物件的,在 Python 的物件到底是什麼東西?模組是怎麼載入的?以及 Python 的記憶體是怎麼管理之類的。
從哪裡開始?
取得原始碼
雖然整個 CPython 專案都可以直接在 GitHub 網站上看的到,但追原始碼這件事,建議還是把整個 CPython 的原始碼拉一份到自己電腦裡慢慢研究比較有效率。如果不知道怎麼使用 Git 下載原始碼的話,可以參考我另一本書「為你自己學 Git」的介紹。
另外,我這整個系列單元使用的 CPython 版本都是 3.12.6
,不同的版本之間的原始碼可能會差滿多的,特別是 Minor 版本以上的變動會差更多。所以如果你想要跟著我一起追原始碼的話,建議跟我使用相同版本,比較能確保我們可以看到一樣的內容。
因為 CPython 的原始碼可能有時候會有點複雜,所以我可能會視情況省略部份程式碼,例如原本可能像這樣:
struct _object {
_PyObject_HEAD_EXTRA
#if (defined(__GNUC__) || defined(__clang__)) \
&& !(defined __STDC_VERSION__ && __STDC_VERSION__ >= 201112L)
// On C99 and older, anonymous union is a GCC and clang extension
__extension__
#endif
#ifdef _MSC_VER
// Ignore MSC warning C4201: "nonstandard extension used:
// nameless struct/union"
__pragma(warning(push))
__pragma(warning(disable: 4201))
#endif
union {
Py_ssize_t ob_refcnt;
#if SIZEOF_VOID_P > 4
PY_UINT32_T ob_refcnt_split[2];
#endif
};
#ifdef _MSC_VER
__pragma(warning(pop))
#endif
PyTypeObject *ob_type;
};
中間有好幾個 #ifdef
的條件編譯指令,有些可能跟作業系統有關,有些跟 Debug 資訊有關,這些大部份時候可能跟我們要講的重點比較沒關係,所以我會視情況把它拿掉,原本的程式碼看起來會變成這樣:
struct _object {
_PyObject_HEAD_EXTRA
union {
Py_ssize_t ob_refcnt;
};
PyTypeObject *ob_type;
};
別誤會,並不是那些拿掉的東西不重要,而是把這些判斷拿掉比較能讓重點更聚焦。
開發工具
不少開發工具都能用來追原始碼,例如 Vim、Visual Studio Code(以下簡稱 VSCode)這樣的文字編輯器,或是 Visual Studio 之類功能完整的 IDE,這些工具都有很好的原始碼閱讀功能,例如很快的跳到函數、巨集或是常數的定義或是搜尋關鍵字等等,都能幫助我們更有效率的閱讀原始碼。
雖然各位從文字可能看不出來,但在這個系列文章中,我會使用 VSCode 來追原始碼,如果你還沒有安裝的話,可以到 VSCode 官網 下載安裝。
除了 VSCode 之外,最近我有個新歡叫做 Zed,這是用 Rust 開發的文字編輯器,效能比 VSCode 好很多,特別遇到檔案比較多或檔案比較大的時候,效能表現是肉眼可見的差別,也推薦給各位。
不會 C 語言看的懂嗎?
先說結論:就算沒有 100% 看懂也沒關係,不求甚解也無妨,閱讀原始碼的過程還是能學到不少東西。
學習並不是一直線的,不需要每行程式碼都看懂才會有所收穫,有時候只是看看原始碼的結構、函數的呼叫、變數的命名就能學到東西。就像拼圖一樣,不 一定要把每塊拼圖都拼完,只要拼出個大概輪廓,也看的出來這幅拼圖長什麼樣子。
別誤會,我也不會 C 語言,幸運的是這個世代有 ChatGPT 這些厲害的工具可以抱大腿,遇到看不懂的就丟給 GPT 幫忙解釋一下,我也是邊看原始碼的過程一邊學 C 語言,就算是瞎子摸象,摸久後也能摸出一些東西來。
其實整個 CPython 專案也不是全部都是用 C 語言寫的,裡面有些模組是用 Python 寫的,如果你原本就會一些 Python,也可以試著從這些模組試試看,應該會比較友善一些。
不過如果你是完完全全的新手,沒寫過任何一種程式語言,可能會有點難跟上,因此我會預期你對程式語言有一些基本的認識,不一定要是 Python,會其它程式語言也可以,知道什麼是變數、函數、迴圈、流程控制之類的基本觀念應該差不多就夠了。
因為在閱讀原始碼的過程中我可能會試著用 printf()
函數印一些資料出來看看,所以可能也得學一下 C 語言的 Hello World 怎麼寫。
所以,接下來我們就動手來寫個 C 語言版的 Hello World 吧!
Hello World
哇!怎麼才第一章就要動手寫程式啦!別怕別怕,在 C 語言寫 Hello World 沒想像中的難,就跟平常各位在 Python 要寫 Hello World 差沒多少。在 Python 你大概會建立一個 .py
檔一樣,在 C 語言就新增一個 .c
的檔案就好,這裡我就讓它叫做 hello.c
,你想換成別的檔名也可以。檔案內容如下:
#include <stdio.h>
int main()
{
printf("Hello World\n");
return 0;
}
- 因為待會要使用
printf()
函數來印點東西出來,這需要引入 C 語言的函式庫<stdio.h>
,這裡包含了執行printf()
函數所需要的宣告。 main()
是整個程式的主要進入點,每個 C 程式都規定要有一個main
函數,待會執行這個程式的時候會從這裡開始執行。printf()
就是這整段程式碼的主角啦,由它把Hello World
給印出來。- 最後的
return 0
表示程式結束,在慣例上如果回傳一個非 0 的值通常表示執行有問題,這裡我就回傳一個0
表示正常執行完畢。
不像 Python 或 JavaScript 可以直接執行 .py
或 .js
檔,C 語言需要先把 .c
檔進行編譯成可執行檔才能執行。
C 語言的編譯器有好幾款,而且不同的作業系統各有不同的編譯器,像是 clang
或是 gcc
,在 Windows 上的話可找看看 Visual Studio(注意,不是 VSCode 喔), 或是安裝 WSL(Windows Subsystem for Linux)來使用 Linux 上的編譯器,更多詳細資訊可參考 Python Developer's Guide 網站的介紹。同時,這個網站上也有很多關於 CPython 相關的文件,有興趣的話可以看看。
我的電腦是 macOS,這裡我用 clang
對我剛才寫的 hello.c
進行編譯:
$ clang hello.c -o helloworld
後面的 -o
是指要把原本的 hello.c
編譯並輸出成 helloworld
執行檔,如果編譯過程沒出錯的話,應該會在當前的目錄看到這個檔案,然後就可以執行了:
$ ./helloworld
Hello World
恭喜!你的 C 語言初體驗就完成了!
下個章節,我們就一起來看一下 CPython 的專案結構,順便在你的電腦編譯 CPython 原始碼!