跳至主要内容

來讀 CPython 原始碼

為你自己學 Python

什麼是 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 的原始碼可能有時候會有點複雜,所以我可能會視情況省略部份程式碼,例如原本可能像這樣:

檔案:Include/object.h
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 資訊有關,這些大部份時候可能跟我們要講的重點比較沒關係,所以我會視情況把它拿掉,原本的程式碼看起來會變成這樣:

檔案:Include/object.h
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,你想換成別的檔名也可以。檔案內容如下:

檔案: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 原始碼!

工商服務

想學 Python 嗎?我教你啊 :)

想要成為軟體工程師嗎?這不是條輕鬆的路,除了興趣之外,還需要足夠的決心、設定目標並持續學習,我們的ASTROCamp 軟體工程師培訓營提供專業的前後端課程培訓,幫助你在最短時間內建立正確且扎實的軟體開發技能,有興趣而且不怕吃苦的話不妨來試試看!