教你閱讀 Cpython 的源碼(一)

以下文章來源於Python學習開發 ,作者陳祥安

Python學習開發

一個喜歡研究python語言的公眾號,主要領域是爬蟲,以及python的使用技巧,乾貨分享。人生苦短,朋友你還不來學python麼


教你閱讀 Cpython 的源碼(一)


正文共:12920 字 7 圖

預計閱讀時間: 33 分鐘

來源:https://realpython.com/cpython-source-code-guide,

譯者:陳祥安

就如同題目一樣,這篇文章就是教你瞭解Cpython的一篇文章。因為內容太長了打算先分開寫,後期看看再合併。

前言

這篇文章很長但是很有用,如果你決定要學習 Cpython,那麼希望你能看下去,你會發現這是一份不錯的學習資料。

這篇文章總共分為 5 部分,你可以根據自己的情況合理的安排閱讀時間。每一部分都要花一定的時間,通過自己去研究這裡面的一些案例,你會感到一種成就感,因為你掌握了 Python 的核心概念,這使得你成為一名更好的 Python 程序員。

第一部分 介紹 Cpython

我們平時說的 Python,其實大多都是指的 Cpython,CPython 是眾多 Python 中的一種,除此之外還有 Pypy,Jpython 等。CPython 同樣的作為官方使用的 Python 版本,以及網上的眾多案例。所以,這裡我們主要說的是 Cpython。

注意:本文是針對 CPython 源代碼的 3.8.0b3 版編寫的。

源代碼中有什麼?

CPython 源代碼分發包含各種工具,庫和組件。我們將在本文中探討這些內容。

首先,我們將重點關注編譯器。先從 git 上下載 Cpython 源代碼.

git clone https://github.com/python/cpython
cd cpython
git checkout v3.8.0b3 #切換我們需要的分支

注意:如果你沒有 Git,可以直接從 GitHub 網站下載 ZIP 文件中的源代碼。

解壓我們下載的文件,其目錄結構如下:

cpython/

├── Doc ← 源代碼文檔說明
├── Grammar ← 計算機可讀的語言定義
├── Include ← C 語言頭文件(頭文件中一般放一些重複使用的代碼)
├── Lib ← Python 寫的標準庫文件
├── Mac ← Mac 支持的文件
├── Misc ← 雜項
├── Modules ← C 寫的標準庫文件
├── Objects ← 核心類型和對象模塊

├── Parser ← Python 解析器源碼
├── PC ← Windows 編譯支持的文件
├── PCbuild ← 老版本的 Windows 系統 編譯支持的文件
├── Programs ← Python 可執行文件和其他二進制文件的源代碼
├── Python ← CPython 解析器源碼
└── Tools ← 用於構建或擴展 Python 的獨立工具

接下來,我們將從源代碼中編譯 CPython。

此步驟需要 C 編譯器和一些構建工具。不同的系統編譯方法也不同,這裡我用的是 mac 系統。

在 macOS 上編譯 CPython 非常簡單。在終端內,運行以下命令即可安裝 C 編譯器和工具包:

$ xcode-select --install


此命令將彈出一個提示,下載並安裝一組工具,包括 Git,Make 和 GNU C 編譯器。

你還需要一個 OpenSSL 的工作副本,用於從 PyPi.org 網站獲取包。

如果你以後計劃使用此版本來安裝其他軟件包,則需要進行 SSL 驗證。

在 macOS 上安裝 OpenSSL 的最簡單方法是使用 HomeBrew。

如果已經安裝了 HomeBrew,則可以使用 brew install 命令安裝 CPython 的依賴項。

$ brew install openssl xz zlib


現在你已擁有依賴項,你可以運行 Cpython 目錄下的 configure 腳本:

$ CPPFLAGS="-I$(brew --prefix zlib)/include" \\
LDFLAGS="-L$(brew --prefix zlib)/lib" \\
./configure --with-openssl=$(brew --prefix openssl) --with-pydebug

上面的安裝命令中,

CPPFLAGS 是 c 和 c++ 編譯器的選項,這裡指定了 zlib 頭文件的位置,

LDFLAGS 是 gcc 等編譯器會用到的一些優化參數,這裡是指定了 zlib 庫文件的位置,

(brew --prefix openssl) 這一部分的意思是在終端裡執行括號裡的命令,顯示openssl 的安裝路徑,可以事先執行括號裡的命令,用返回的結果替換 (brew --prefix openssl),效果是一樣的,每一行行尾的反斜槓可以使換行時先不執行命令,而是把這三行內容當作一條命令執行。

運行完上面命令以後在存儲庫的根目錄中會生成一個 Makefile,你可以使用它來自動化構建過程。./configure步驟只需要運行一次。

你可以通過運行以下命令來構建 CPython 二進制文件。

$ make -j2 -s


-j2 標誌允許 make 同時運行 2 個作業。如果你有 4 個內核,則可以將其更改為 4. -s 標誌會阻止 Makefile 將其運行的每個命令打印到控制檯。你可以刪除它,輸出的東西太多了。在構建期間,你可能會收到一些錯誤,在摘要中,它會通知你並非所有包都可以構建。

例如,_dbm,_sqlite3,_uuid,nis,ossaudiodev,spwd 和_tkinter 將無法使用這組指令構建。如果你不打算針對這些軟件包進行開發,這個錯誤沒什麼影響。如果你實在需要可以參考:https://devguide.python.org/。

構建將花費幾分鐘並生成一個名為 python.exe 的二進制文件。每次改動源代碼,都需要重新運行 make 進行編譯。

python.exe 二進制文件是 CPython 的調試二進制文件。執行下面命令可以看到 Python 的運行版本。

$ ./python.exe
Python 3.8.0b3 (tags/v3.8.0b3:4336222407, Aug 21 2019, 10:00:03)
[Clang 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

(其實最新的已經到 Python3.9 了,我編譯了一下效果如下)

教你閱讀 Cpython 的源碼(一)


編譯器做了什麼?

編譯器的目的就是將一種語言轉為另外一種語言。可以把編譯的過程比作翻譯,把英語裡的“Hello”,翻譯成中文的「你好」。

一些編譯器將代碼編譯成只有機器看懂的機器代碼,可以直接在系統上進行執行。其他編譯器將編譯成中間語言,由虛擬機執行。

選擇編譯器時做出的一個重要決定是系統可移植性要求。Java 和.NET CLR 將編譯成中間語言,以便編譯的代碼可適配其他系統類型。C,Go,C ++和 Pascal 將編譯成一個低級可執行文件,只能在類似於編譯的系統上運行。

我們一般會直接發佈 Python 的源代碼,然後直接通過 Python 命令即可運行,其實在內部,運行時 CPython 會編譯你的代碼。大多數認為 Python 是一種解釋性語言。

嚴格來說其實它實際上是編譯類型。

Python 代碼不會編譯成機器代碼。

它被編譯成一種特殊的低級中間語言,只有 CPython 才能理解的字節碼。在 Python3 中字節碼就存儲在隱藏目錄中的.pyc 文件中,提供了緩存以供下次快速執行。所以,如果在不更改源代碼的情況下運行相同的 Python 應用程序兩次,第二次總是會快得多。原因就是第二次的時候直接加載了字節碼然後運行了程序,不像第一次還需要編譯。

為什麼 CPython 是用 C 而不是 Python 編寫的?

CPython 中的 C 是對 C 編程語言的引用,暗示這個 Python 發行版是用 C 語言編寫的。

CPython 中的編譯器是用純 C 編寫的。但是,許多標準庫模塊都是用純 Python 或 C 和 Python 的組合編寫的。

那麼為什麼 CPython 是用 C 而不是 Python 編寫的?

答案就在於編譯器的工作原理。

編譯器有兩種類型:

  • 自託管編譯器是用它們編譯的語言編寫的編譯器,例如 Go 編譯器。
  • 源到源編譯器是用另一種已經有編譯器的語言編寫的編譯器。
  • 這也就意味著如果從頭開始編寫新的編程語言,則需要一個可執行的應用程序來編譯你的編譯器!你就需要一個編譯器來執行任何操作,因此在開發新語言時,它們通常首先用較舊的,更成熟的語言編寫。同時節省時間和學習成本。
  • 一個很好的例子就是 Go 語言。
  • 第一個 Go 編譯器是用 C 編寫的,然後 Go 可以編譯,編譯器就在 Go 中重寫了。

CPython 保留了它的 C 的特性:許多標準庫模塊(如 ssl 模塊或 sockets 模塊)都是用 C 語言編寫的,用於訪問低級操作系統 API。

用於創建網絡套接字,與文件系統一起工作或與顯示器交互的 Windows 和 Linux 內核中的 API 都是用 C 語言編寫的。所以將 Python 的可擴展性層專注於 C 語言是有意義的。在本文的後面部分,我們將介紹 Python 標準庫和 C 模塊。除此之外,有一個用 Python 編寫的 Python 編譯器叫做 PyPy。

PyPy 的徽標是一個 Ouroboros,代表編譯器的自託管特性。另一個 Python 交叉編譯器的例子是 Jython。

還有一個就是 Jython。Jython 是用 Java 編寫的,從 Python 源代碼編譯成 Java 字節碼。與 CPython 可以輕鬆導入 C 庫並從 Python 中使用它們一樣,Jython 使得導入和引用 Java 模塊和類變得容易。

Python 語言規範

CPython 源代碼中包含的是 Python 語言的定義。這是所有 Python 解釋器使用的參考規範。該規範採用人類可讀和機器可讀的格式。文檔內部詳細說明了 Python 語言,允許的內容以及每個語句的行為方式。

文檔

位於Doc/reference目錄內的是reStructuredText文件解釋了 Python 語言中每個功能屬性。這構成了docs.python.org上的官方 Python 參考指南。

在目錄中是你需要了解整個語言,結構和關鍵字的文件:

cpython/Doc/reference
|
├── compound_stmts.rst
├── datamodel.rst
├── executionmodel.rst
├── expressions.rst
├── grammar.rst
├── import.rst
├── index.rst
├── introduction.rst
├── lexical_analysis.rst
├── simple_stmts.rst
└── toplevel_components.rst

在compound_stmts.rst文件中,你可以看到一個定義 with 語句的簡單示例。with 語句可以在 Python 中以多種方式使用,最簡單的是上下文管理器的實例化和嵌套的代碼塊:

with x():
...

你可以使用 as 進行重命名

with x() as y:
...

你還可以鏈式的同時定義多個

with x() as y, z() as jk:
...

接下來,我們將探索 Python 語言的計算機可讀文檔。

Grammar

該文檔包含人類可讀規範和存放在單個文件Grammar/Grammar中的機器可讀規範。

Grammar 文件是使用稱為 Backus-Naur Form(BNF)的上下文表示法進行編寫的。

BNF 不是特定於 Python 的,並且通常用作許多其他語言中語法的符號。

編程語言中的語法結構概念是從 20 世紀 50 年代Noam Chomsky’s work on Syntactic Structures中受到啟發的。

Python 的語法文件使用具有正則表達式語法的 Extended-BNF(EBNF)規範。

所以,在語法文件中你可以使用:

  • *重複
  • +至少重複一次
  • []為可選部分
  • |任選一個
  • ()用於分組
  • 如果在語法文件中搜索 with 語句,你將看到 with 語句的定義:
.. productionlist::
with_stmt: "with" `with_item` ("," `with_item`)* ":" `suite`
with_item: `expression` ["as" `target`]

引號中的內容都是字符串,這是一中關鍵字的定義方式。所以 with_stmt 指定為:

1.with單詞開頭

2.接下來是 with_item,它是一個test和(可選)as 表達式。

3.多個項目之間使用逗號進行間隔

4.以字符:結尾

5.其次是 suite。

這兩行中提到了一些其他定義:

  • suite是指具有一個或多個語句的代碼塊。
  • test是指一個被評估的簡單語句。
  • expr指的是一個簡單的表達式
  • 如果你想詳細探索這些內容,可以在此文件中定義整個 Python 語法。

如果你想看一個最近如何使用語法的例子,例如在 PEP572 中,:=運算符被添加到語法文件中。

 ATEQUAL '@='
RARROW '->'
ELLIPSIS '...'
+ COLONEQUAL ':='
OP
ERRORTOKEN

使用 pgen

Grammar 文件本身不會被 Python 編譯器使用。

是使用一個名為 pgen 的工具,來創建的解析器表。pgen 會讀取語法文件並將其轉換為解析器表。如果你對語法文件進行了更改,則必須重新生成解析器表並重新編譯 Python。

注意:pgen 應用程序在 Python 3.8 中從 C 重寫為純 Python。


為了查看 pgen 的運行情況,讓我們改變 Python 語法的一部分。並重新編譯運行 Python。

在 Grammar 路徑下看到兩個文件 Grammar 和 Tokens,我們在 Grammar 搜索pass_stmt,然後看到下面這樣

pass_stmt: 'pass'


我們修改一下,改為下面這樣

pass_stmt: 'pass' | 'proceed'


在 Cpython 的根目錄使用make regen-grammar命令來運行pgen重新編譯 Grammar 文件。

應該看到類似於此的輸出,表明已生成新的Include/graminit.h和Python/graminit.c文件:

下面是部分輸出內容

# Regenerate Include/graminit.h and Python/graminit.c
# from Grammar/Grammar using pgen
PYTHONPATH=. python3 -m Parser.pgen ./Grammar/Grammar \\
./Grammar/Tokens \\
./Include/graminit.h.new \\
./Python/graminit.c.new
python3 ./Tools/scripts/update_file.py ./Include/graminit.h ./Include/graminit.h.new
python3 ./Tools/scripts/update_file.py ./Python/graminit.c ./Python/graminit.c.new

使用重新生成的解析器表,需要重新編譯 CPython 才能查看新語法。使用之前用於操作系統的相同編譯步驟。

make -j4 -s


如果代碼編譯成功,執行新的 CPython 二進制文件並啟動 REPL。

./python.exe


在 REPL 中,現在可以嘗試定義一個函數,使用編譯為 Python 語法的 proceed 關鍵字替代 pass 語句。

Python 3.8.0b3 (tags/v3.8.0b3:4336222407, Aug 21 2019, 10:00:03) 
[Clang 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> def example():
... proceed
...
>>> example()

下面是我運行結果,很有意思居然沒有出錯。

教你閱讀 Cpython 的源碼(一)


接下來,我們將探討 Tokens 文件及其與 Grammar 的關係。

Tokens

與 Grammar 文件夾中的語法文件一起是一個 Tokens 文件,它包含在解析樹中作為葉節點找到的每個唯一類型,稍後我們將深入介紹解析器樹。每個 token 還具有名稱和生成的唯一 ID,這些名稱用於簡化在 tokenizer 中引用。

注意:Tokens 文件是 Python 3.8 中的一項新功能。


例如,左括號稱為 LPAR,分號稱為 SEMI。

你將在本文後面看到這些標記:

LPAR '('
RPAR ')'
LSQB '['
RSQB ']'
COLON ':'
COMMA ','
SEMI ';'

與語法文件一樣,如果更改 Tokens 文件,則需要再次運行 pgen。

要查看操作中的 tokens,可以在 CPython 中使用 tokenize 模塊。創建一個名為 test_tokens.py 的簡單 Python 腳本:

# Hello world!
def my_function():
proceed

然後通過名為 tokenize 的標準庫中內置的模塊傳遞此文件。你將按行和字符查看令牌列表。使用-e 標誌輸出確切的令牌名稱:

0,0-0,0: ENCODING 'utf-8' 
1,0-1,14: COMMENT '# Hello world!'
1,14-1,15: NL '\\n'
2,0-2,3: NAME 'def'
2,4-2,15: NAME 'my_function'
2,15-2,16: LPAR '('
2,16-2,17: RPAR ')'
2,17-2,18: COLON ':'
2,18-2,19: NEWLINE '\\n'
3,0-3,3: INDENT ' '
3,3-3,7: NAME 'proceed'
3,7-3,8: NEWLINE '\\n'
4,0-4,0: DEDENT ''
4,0-4,0: ENDMARKER ''


教你閱讀 Cpython 的源碼(一)


在輸出中,第一列是行/列座標的範圍,第二列是令牌的名稱,最後一列是令牌的值。

在輸出中,tokenize 模塊隱含了一些不在文件中的標記。

utf-8 的 ENCODING 標記,末尾有一個空行,DEDENT 關閉函數聲明,ENDMARKER 結束文件。tokenize 模塊是用純 Python 編寫的,位於 CPython 源代碼中的Lib/tokenize.py中。

重要提示:CPython 源代碼中有兩個 tokenizers:一個用 Python 編寫,上面演示的這個,另一個是用 C 語言編寫的。用 Python 編寫的被用作實用程序,而用 C 編寫的被用於 Python 編譯器。但是,它們具有相同的輸出和行為。用 C 語言編寫的版本是為性能而設計的,Python 中的模塊是為調試而設計的。


要查看 C 語言的的 tokenizer 的詳細內容,可以使用-d 標誌運行 Python。

使用之前創建的 test_tokens.py 腳本,使用以下命令運行它:

./python.exe -d test_tokens.py


得到如下結果

Token NAME/'def' ... It's a keyword
DFA 'file_input', state 0: Push 'stmt'
DFA 'stmt', state 0: Push 'compound_stmt'
DFA 'compound_stmt', state 0: Push 'funcdef'
DFA 'funcdef', state 0: Shift.
Token NAME/'my_function' ... It's a token we know
DFA 'funcdef', state 1: Shift.
Token LPAR/'(' ... It's a token we know
DFA 'funcdef', state 2: Push 'parameters'
DFA 'parameters', state 0: Shift.
Token RPAR/')' ... It's a token we know
DFA 'parameters', state 1: Shift.
DFA 'parameters', state 2: Direct pop.
Token COLON/':' ... It's a token we know
DFA 'funcdef', state 3: Shift.
Token NEWLINE/'' ... It's a token we know
DFA 'funcdef', state 5: [switch func_body_suite to suite] Push 'suite'
DFA 'suite', state 0: Shift.
Token INDENT/'' ... It's a token we know
DFA 'suite', state 1: Shift.
Token NAME/'proceed' ... It's a keyword
DFA 'suite', state 3: Push 'stmt'
...
ACCEPT.

在輸出中,您可以看到它突出顯示為關鍵字。在下一章中,我們將看到如何執行 Python 二進制文件到達 tokenizer 以及從那裡執行代碼會發生什麼。現在您已經概述了 Python 語法以及 tokens 和語句之間的關係,有一種方法可以將 pgen 輸出轉換為交互式圖形。

以下是 Python 3.8a2 語法的屏幕截圖:


教你閱讀 Cpython 的源碼(一)


看不清沒關係,用於生成此圖的 Python 包(instaviz)將在後面的章節中介紹。這裡先做了解。

Python 中的內存管理

在本文中,你將看到對 PyArena 對象的引用。

arena是 CPython 的內存管理結構之一。代碼在Python/pyarena.c中其中包含了 C 的內存分配和解除分配的方法。

在編寫的 C 程序中,開發人員應在寫入數據之前為數據結構分配內存。此分配將內存標記為屬於操作系統的進程。當不再使用已分配的內存並將其返回到操作系統的可用內存塊表時,開發人員也可以解除分配或“釋放”它們。如果進程為一個變量分配內存,比如在函數或循環中,當該函數完成時,內存不會自動返回給 C 中的操作系統。因此,如果它未在 C 代碼中顯式釋放,則會導致內存洩漏。每次該函數運行時,該過程將繼續佔用更多內存,直到最終,系統耗盡內存並崩潰!Python 將這一責任從程序員手中奪走,並使用兩種算法:引用計數器和垃圾收集器。每當解釋器被實例化時,PyArena方法創建並附加解釋器中的一塊內存區域。在 CPython 解釋器的生命週期中,arenas可以被分配。它們與鏈表相關聯。

arenas將 Python 對象的指針列表存儲為PyListObject方法。每當創建一個新的 Python 對象時,都會使用PyArena_AddPyObject方法添加指向它的指針。

此函數調用將指針存儲在arenas列表 a_objects 中。PyArena方法提供第二個功能,即分配和引用原始內存塊列表。例如,如果添加了數千個附加值,C 代碼中PyList將需要額外的內存。但是PyList不直接分配內存。該對象通過從PyObject調用具有所需內存大小的PyArena_Malloc從PyArena獲取原始內存塊。此任務在Objects/oballoc.c中的完成。在對象分配模塊中,可以為 Python 對象分配,釋放和重新分配內存。已分配塊的鏈接列表存儲在arenas內,因此當解釋器停止時,可以使用PyArena_Free一次解除所有託管內存塊的釋放。

以PyListObject為例,如果你使用.append()一將個對象放到 Python 列表的末尾,就不需要重新分配內存了,而是使用現有列表中內存。

.append()方法調用list_resize()來處理列表的內存分配。每個列表對象都保留已分配內存量的列表。如果要追加的項目將適合現有的可用內存,則只需添加即可。如果列表需要更多內存空間,則會進行擴展。列表的長度擴展為 0,4,8,16,25,35,46,58,72,88。

調用PyMem_Realloc可以擴展列表中分配的內存。

PyMem_Realloc是pymalloc_realloc的 API 包裝器。Python 還有一個 C 調用malloc的特殊包裝器,它設置內存分配的最大大小以幫助防止緩衝區溢出錯誤(參見 PyMem_RawMalloc)。

綜上所述:

  • 原始內存塊的分配是通過PyMem_RawAlloc完成的。
  • Python 對象的指針存儲在PyArena中。
  • PyArena還存儲了已分配內存塊的鏈表。
  • 有關 API 的更多信息,請參閱 CPython 文檔。

引用計數

要在 Python 中創建變量並賦值,變量名必須為一。

my_variable = 180392


只要在 Python 中為變量賦值,就會在 locals 和 globals 範圍內檢查變量的名稱,以查看它是否已存在。因為 my_variable 不在 locals()或 globals()字典中,所以創建了這個新對象,並將該值指定為數字常量 180392。現在有一個對 my_variable 的引用,因此 my_variable 的引用計數器增加 1。

你可以在 CPython 的 C 源代碼中看到函數Py_INCREF和Py_DECREF。

這兩個函數分別是對該對象的遞增和遞減做引用計數。當變量超出聲明範圍時,對對象的引用會遞減。Python 中的範圍可以指代函數或方法,生成式或 lambda 函數。這些是一些更直觀的範圍,但還有許多其他隱式範圍,比如將變量傳遞給函數調用。遞增和遞減引用的處理在 CPython 編譯器和核心執行循環ceval.c文件中。我們將在本文後面詳細介紹。

每當調用Py_DECREF並且計數器變為 0 時,就會調用PyObject_Free函數。對於該對象,會為所有已分配的內存調用PyArena_Free。

垃圾收集

CPython 的垃圾收集器默認啟用,發生在後臺,用於釋放已不再使用的對象的內存。

因為垃圾收集算法比引用計數器複雜得多,所以它不會一直髮生,否則會消耗大量的 CPU 資源。經過一定數量的操作後,它會定期發生。CPython 的標準庫附帶了一個 Python 模塊,用於與arena和垃圾收集器 gc 模塊連接。

以下是在調試模式下使用 gc 模塊的方法:

>>> import gc
>>> gc.set_debug(gc.DEBUG_STATS)

這將在運行垃圾收集器時打印統計信息。

可以通過調用get_threshold來獲取運行垃圾收集器的閾值:

>>> gc.get_threshold()
(700, 10, 10)

還可以獲取當前閾值計數:

>>> gc.get_count() 

(688, 1, 1)

最後,你可以手動運行收集算法:

>>> gc.collect()
24

這將調用Modules/gcmodule.c文件中的collect(),該文件包含垃圾收集器算法的實現。

結論

在第 1 部分中,我們介紹了源代碼庫的結構,如何從源代碼編譯以及 Python 語言規範。

當你深入瞭解 Python 解釋器過程時,這些核心概念在第 2 部分中將是至關重要的。

-後續-


分享到:


相關文章: