# 鐵人賽 - 為你自己讀 CPython 原始碼

> 這次的 iThome 鐵人賽，我給自己選了一個有點硬的主題，就是閱讀 CPython 的原始碼...

Published: 2024-10-15
URL: https://kaochenlong.com/dive-into-cpython-source-code

---

這次的 iThome [鐵人賽](https://ithelp.ithome.com.tw/users/20065770/ironman/7541)，我給自己選了一個有點硬的主題，就是閱讀 CPython 的原始碼。

今年也剛好在 PyCon Taiwan 有一場工作坊，主題是介紹如何閱讀 CPython 的原始碼，所以原本只是想藉著鐵人賽的活動，把工作坊的內容文字化做為補充資料，沒想到越寫越多，對我這個不懂 C 語言的人來說更是吃力。雖然現在有很多 AI 工具的輔助，但幾乎每篇文章得要一邊挖原始碼，一邊驗證我解讀的程式碼是不是正確的，每篇文章至少都得花三個小時以上的時間。

&lt;!-- more --&gt;

但做這件事也是有好處的，除了學到了一些入門的 C 語言，就是現在對這個程式語言有更進一步的認識了，就算要跟別人戰程式的話（並沒有）也比較有底氣一點點。而且，我的工作有一半以上都是在講課，萬一被同學問到奇怪的 Python 問題，現在應該都透過翻閱 CPython 的原始碼的實作講的出真正的原因。

另外，我自己有個有點無聊的小小堅持，就是希望這個系列的每篇文章（包括現在大家在看的這篇也是），都能給它一個合適的 Banner 圖片，從一開始像是新兵戰士面對的像火龍一樣強大的對手，現在能跟它和平相處，甚至藉由它可以再回頭看看其它程式語言的實作，能更了解不同程式語言之間的差異，對我來說真的是件很有趣的事 :)

## 漏網之魚？

在時間有限而且能力也有限的情況下，目前只能暫時先寫這 30 篇的文章，但我對以下這些主題也很有興趣：

- 虛擬機器（VM）的實作
- 記憶體管理與資源回收
- GIL，特別是在 3.13 之後 GIL 解禁之後有什麼不一樣

希望在不久的將來，在時間比較寬裕以及能力提昇之後，可以再回來挑戰這些主題 :)

## 主題

[第 1 章：來讀 CPython 原始碼](https://pythonbook.cc/chapters/cpython/introduction)

CPython 是最常見的 Python 實作品，先把專案原始碼下載一份到本機，順便寫一個簡單的 C 語言 Hello World 程式，為後續的 CPython 原始碼學習做準備。

[第 2 章：CPython 專案簡介](https://pythonbook.cc/chapters/cpython/project-structure)

介紹如何編譯並修改 CPython 原始碼以及專案目錄結構，順便範例展示了如何修改 CPython 原始碼，例如在進入 REPL 時顯示「Hello CPython」，離開時顯示「Bye」讓 Python REPL 禮貌地打招呼和道別。

[第 3 章：全部都是物件（上）](https://pythonbook.cc/chapters/cpython/pyobject-part-1)

在 CPython 裡，所有物件都由 PyObject 結構來表示，內含兩個主要成員：ob_refcnt 管理物件的引用計數，當計數降為 0 時，物件會被回收；ob_type 則指向 PyTypeObject，決定物件的型別及行為...

[第 4 章：物件生成全紀錄](https://pythonbook.cc/chapters/cpython/creating-an-object)

介紹從 Python 執行簡單的類別定義與實體化的過程，首先，程式碼經過 Tokenization 和 AST 解析，接著編譯為 Bytecode，再由 Python 虛擬機器執行...

[第 5 章：全部都是物件（下）](https://pythonbook.cc/chapters/cpython/pyobject-part-2)

在 CPython 中，PyTypeObject 是負責定義所有 Python 型別的關鍵結構，裡面包含了物件的名稱、大小、屬性和行為，並使用串列做為範例，展示了這個結構是如何運作。

[第 6 章：我的 Python 會後空翻！](https://pythonbook.cc/chapters/cpython/add-custom-builtin-type)

介紹如何在 CPython 裡面自製一個叫做 PyKitty_Type 的型別，並讓這個型別可以做出打招呼和後空翻的動作。

[第 7 章：匯入模組的時候...](https://pythonbook.cc/chapters/cpython/what-happened-when-importing-module)

Python 的模組匯入機制其實比我們平常看到的複雜許多。當我們使用 import 指令時，Python 會先從 sys.modules 查找已經匯入過的模組，如果沒找到，則透過內建的 importlib 模組來加載模組。import 和 from ... import ... 這兩種寫法在底層執行時略有不同，前者會直接匯入整個模組，而後者則會提取模組中的特定屬性。

[第 8 章：整數的前世今生](https://pythonbook.cc/chapters/cpython/numbers)

當在 Python 裡執行 n = 9527 這樣的程式碼時，背後發生了不少事。首先，數字 9527 會被編譯成 Bytecode，並存放在常數表中，等到執行時再從這裡取出。Python 的設計使它能處理任意大小的整數，因為可以用多個「digit」來分段儲存大數字，打破了其他程式語言的數字上限問題。此外，為了提升效能，Python 預先建立了從 -5 到 256 的小整數，這些整數會重複使用，而不是每次都新建物件。這樣的設計讓 Python 既靈活又高效。

[第 9 章：浮點數之小數點漂流記](https://pythonbook.cc/chapters/cpython/floating-point)

浮點數，顧名思義，它的「小數點」位置是可以浮動的，這樣的設計讓電腦能有效處理非常大的或非常小的數字。不同於日常生活中的小數，電腦內的浮點數是以類似科學記號的方式儲存，使用「有效數字」和「指數」來表示。Python 的浮點數其實就是 C 語言的 double，這是依照 IEEE 754 標準來處理的，因此會有精度不足的問題，這就是為什麼計算浮點數時會出現些許誤差。

[第 10 章：字串的秘密生活（上）](https://pythonbook.cc/chapters/cpython/strings)

當你在 Python 裡寫下 message = &quot;Hello, World!&quot;，背後其實發生了很多事。這段程式碼會將字串「Hello, World!」編譯成 Bytecode 並建立一個字串物件。如果字串是空的，CPython 會從直譯器內部拿出同一個空字串；若字串包含 ASCII 字元，則會使用更節省記憶體的 ASCII 結構。不過，一旦字串含有像 Emoji 這樣的 Unicode 字元，Python 會自動轉換成較大的 Unicode 結構來處理。最重要的是，Python 的字串是不可變的，這表示每次修改字串，實際上都是產生一個新的字串物件。

[第 11 章：字串的秘密生活（下）](https://pythonbook.cc/chapters/cpython/more-about-strings)

在 Python 中，字串的操作其實不像表面看起來那麼簡單。比如字串串接時，雖然像 a = a + &quot;😊&quot; 這樣的操作看似直接，但實際上會產生一個新的字串物件，並透過內部的函數來進行字元的複製與串接。過程中，如果兩個字串的編碼相同，會直接進行記憶體層級的快速拷貝。如果編碼不同，則會轉換編碼並逐字複製，速度會慢一些。

另外，Python 也會利用字串內部化技術來節省記憶體，將常用的字串存入字串池，讓相同內容的字串共用同一塊記憶體，進一步提升效能。

[第 12 章：從準備到起飛！](https://pythonbook.cc/chapters/cpython/what-happens-when-python-starts)

當執行 python hello.py 時，Python 直譯器先讀取程式碼，然後經過一系列的處理步驟，最終執行並顯示結果。在這個過程中，程式碼會先被讀取並轉換成一種可以被理解的結構，接著編譯成可執行的形式，最後由 Python 的執行器執行並輸出「Hello Kitty」...

[第 13 章：參觀 Bytecode 工廠](https://pythonbook.cc/chapters/cpython/from-py-to-pyc)

當執行 Python 程式時，程式碼會被編譯成 Bytecode，並且生成 .pyc 檔案。這個檔案主要是為了加快執行速度，特別是被重複匯入的模組。但執行主程式時，Python 並不會自動生成 .pyc，因為多數情況下主程式只會執行一次，生成 .pyc 的必要性不大。 .pyc 檔案裡面除了存有魔術數字，還包含編譯後的 Bytecode，這些 Bytecode 是由一連串的 opcode 組成，Python 的虛擬機器會根據這些 opcode 來執行程式。

[第 14 章：串列的排隊人生](https://pythonbook.cc/chapters/cpython/lists)

使用 Python 的串列（List）的時候會發現它可以存放不同型態的資料，還可以動態增加或減少元素，而且還不用事先定義大小。這麼方便的設計背後，CPython 是如何實現的？串列的內部結構其實是透過一個指向元素的指針來運作，而不是直接存放資料，因此它能裝下各種型態的物件。而當需要更多記憶體時，CPython 會採取「過度分配」的策略，也就是一次多要一些空間，以減少頻繁重新分配的次數，提升效能。

[第 15 章：字典的整理收納課（上）](https://pythonbook.cc/chapters/cpython/dictionaries)

字典是 Python 中常用的資料結構，Key &amp; Vaule 的組合存取資料，在 CPython 裡，字典的內部結構由 PyDictObject 定義，Key 跟 Value 分別存放在不同的物件中，這樣設計是為了效能與記憶體使用的平衡。

新增元素時，Python 會先透過計算鍵的雜湊值來決定其在字典中的位置，如果發生雜湊碰撞，系統會利用特殊的計算公式來找到下個可用的空位，並將鍵值對存放於對應的記憶體空間內。查找時則會根據計算出的索引快速定位到對應的鍵值。雖然這個流程聽起來有些複雜，但實際運行速度非常快，讓大部分情況下字典的查找效率接近 O(1)。

[第 16 章：字典的整理收納課（下）](https://pythonbook.cc/chapters/cpython/more-about-dictionaries)

在 Python 中，字典的記憶體空間會隨著加入的元素數量自動調整，這是為了在效能和記憶體使用之間取得平衡。當字典裡的空間不足時，系統會根據「負載因子」來決定何時需要擴展容量，通常當使用率超過 2/3 時會自動擴充空間。每次擴展的量約為原本的 2 倍，以減少碰撞機率並確保效能不受影響。然而，即使刪除元素後，已擴展的空間不會自動回收，除非特意清空字典或重新建立...

[第 17 章：不動如山的 Tuple](https://pythonbook.cc/chapters/cpython/tuples)

在 Python 裡 Tuple 是一種不可變的資料結構，意思是它一旦建立後，裡面的元素就不能更改、刪除或新增。建立 Tuple 時，Python 會透過特殊的函數來處理，無論是空的 Tuple 還是有資料的 Tuple，背後都有不同的流程處理。Python 還會使用「空閒列表」來有效管理記憶體回收，像是當元素數量小於 20 時，Python 不會馬上釋放記憶體，而是暫時保留以供未來使用。此外，Tuple 的不可變性意味著你無法直接修改它的內容，這個特性跟字串相似。Python 甚至對某些特定數量元素的 Tuple 做了些有趣的效能最佳化來提升運行效率。

[第 18 章：虛擬機器五部曲（一）](https://pythonbook.cc/chapters/cpython/pvm-code-objects)

在 Python 裡，函數也是物件，當我們定義一個函數時，Python 會建立一個 PyFunctionObject，裡面包含了函數的名字、參數、文件字串等資訊。函數背後最關鍵的部分是一個叫做 Code Object 的東西，這個物件儲存了函數的程式碼。在編譯階段，Python 將我們寫的程式轉換 Bytecode，然後將 Bytecode 包裝成 Code Object，最後由虛擬機器執行。

[第 19 章：虛擬機器五部曲（二）](https://pythonbook.cc/chapters/cpython/pvm-function-objects)

函數物件是透過 MAKE_FUNCTION 指令建立的，裡面包含了 Code Object、函數名稱、預設值等屬性。oparg 參數則用來決定函數物件具備哪些屬性...

[第 20 章：虛擬機器五部曲（三）](https://pythonbook.cc/chapters/cpython/pvm-frame-objects)

當呼叫一個 Python 函數，背後會建立一個叫 Frame 的物件。這個 Frame 會儲存像是區域變數、全域變數等資料，並形成一個堆疊結構，每次呼叫函數就會加上一個新的 Frame。當函數執行完畢後，這個 Frame 就會被清理掉。在 CPython 裡，Frame 包含許多重要的資訊，比如指向函數的程式碼、變數、甚至前一個 Frame，確保執行流程順暢，然後在適當時機被銷毀，讓記憶體資源得以回收。

[第 21 章：虛擬機器五部曲（四）](https://pythonbook.cc/chapters/cpython/pvm-namespaces-and-scopes)

當 Python 執行程式碼時，它會根據 LEGB（Local、Enclosing、Global、Built-in）的規則來查找變數，大家可能不知道的是，其實 Python 的編譯器會在程式執行前，就已經決定該如何查找變數了...

[第 22 章：虛擬機器五部曲（五）](https://pythonbook.cc/chapters/cpython/pvm-cell-and-closure)

Python 中的閉包（Closure）是怎麼運作的？閉包允許內層函數存取外層函數的變數，而這些變數會被包裝成所謂的 Cell Object。當我們定義一個包含閉包的函數時，Python 會自動建立這些 Cell，並將外層的變數放入其中，讓內層函數可以在之後使用。

[第 23 章：類別與它們的產地](https://pythonbook.cc/chapters/cpython/class-and-metaclass)

物件導向設計中，類別是用來建立實體（物件）的，但在 Python 世界裡，類別本身也是物件。那麼類別本身又是誰創造的？其實所有的類別都是透過它們的 metaclass 來建立。Python 裡的 class 語法背後其實是使用內建函數 __build_class__() 來完成這個過程，而 type 類別本身的 metaclass 也還是 type，這樣就形成了類別的遞迴關係。

[第 24 章：繼承與家族紛爭（上）](https://pythonbook.cc/chapters/cpython/class-and-inheritance)

在大多數程式語言裡，繼承可能沒那麼複雜，基本上就是把共用的功能寫在上層類別，然後讓下層類別繼承它就好了。但 Python 的繼承多了一個「多重繼承」，也就是一個類別可以同時繼承多個上層類別，讓整個結構複雜不少。

當 Python 要找到一個物件的方法時，會依據「MRO」（方法解析順序）來決定要先去哪一個上層類別找，這個順序是用一套規則計算出來的。當然，平常只用單一繼承時，你不會覺得有什麼不同，但多重繼承的情況下，MRO 就會幫忙決定方法的查找順序。

[第 25 章：繼承與家族紛爭（中）](https://pythonbook.cc/chapters/cpython/mro-and-c3-linearization)

Python 採用了 C3 線性演算法來解決方法繼承順序的問題。這個演算法能找出一個符合規則的單調次序，確保在多重繼承下方法的呼叫順序是合理的。

透過 MRO 可以看到 Python 是依據 C3 線性演算法決定方法查找的順序，先看上層，再看平行層，最後會到最上層類別 object。不過當遇到無法確定順序的繼承關係時，Python 會拋出錯誤。

[第 26 章：繼承與家族紛爭（下）](https://pythonbook.cc/chapters/cpython/mro-and-super)

MRO 是透過 C3 線性演算法來解決多重繼承的順序問題，而 super() 則是根據這個順序從 MRO 中的下一個類別開始查找方法。特別的是，Python 的 super() 並不是單純呼叫上層類別的方法，而是透過一個代理物件來動態解析方法，確保不會跳過任何一個繼承的類別，從而避免了多重繼承時的「鑽石問題」。

[第 27 章：產生一個產生器](https://pythonbook.cc/chapters/cpython/yield-and-generator)

產生器在 Python 裡的運作很好玩。簡單來說，透過 yield 關鍵字，可以讓函數暫停並回傳值，這對處理大量資料或無限集合特別實用。產生器的內部狀態會記錄在 PyGenObject 裡，包含名稱、例外狀態、以及執行狀態。雖然產生器能省下記憶體，但它的實作有點複雜，特別是在管理狀態和例外處理的部分。

[第 28 章：轉呀轉呀七彩迭代器](https://pythonbook.cc/chapters/cpython/iterators)

迭代器在 Python 裡非常常見，它讓我們能很簡單的遍歷不同類型的「容器」，像是字串、串列、Tuple 或是字典等結構。Python 中的迭代器只要有遵循「迭代器協議」就能被稱為迭代器。Python 的 iter() 函數背後有幾個有趣的實作細節，例如它支援一種「哨兵」機制，可以用來指定當某個條件達成時停止迭代。不同的資料型態，如串列、字典和範圍，都有各自的迭代器實作方式。

[第 29 章：無所不在的描述器](https://pythonbook.cc/chapters/cpython/descriptor)

描述器（Descriptor）其實是 Python 中很常見的東西，只是你可能不知道自己已經在用了。它可以控制屬性的存取，讓我們在讀取或設定屬性時，背後做一些額外的事。描述器有兩種：資料描述器和非資料描述器，區別在於是否實作了特定的方法...

[第 30 章：例外處理的幕後真相](https://pythonbook.cc/chapters/cpython/exceptions)

Python 處理例外的方式很直觀，使用 try...except 就能應付各種可能的錯誤。當然，這背後在 CPython 裡可是有一整套機制運作。當發生例外時，Python 會把錯誤資訊堆疊起來，方便之後的處理。除此之外，Bytecode 也會包含一些用來追蹤例外的表格（ExceptionTable），確保我們能跳到正確的處理位置。最後，finally 區塊更是保證不論發生什麼事都會被執行，讓程式有條理地結束...

