Python 的 compile()
函式允許在程式碼中呼叫編譯器,並傳回程式碼物件,其中包含編譯後的 bytecode。理解程式碼物件的結構和 bytecode 指令對於深入理解 Python 執行機制至關重要。dis
模組提供反組譯 bytecode 的功能,可以幫助開發者分析程式碼的執行流程。Python 編譯過程涉及詞法分析、語法分析、語義分析、中間碼生成、最佳化和程式碼生成等步驟。編譯器使用深度優先搜尋(DFS)演算法來遍歷控制流程圖(CFG),CFG 描述了程式的控制流程,每個節點代表一個基本塊,邊代表控制流程的跳轉。基本塊是編譯器最佳化的基本單位。編譯器將中間碼轉換為機器碼,並生成 PyCodeObject 物件,其中包含 bytecode 和其他相關資訊。為了實作「幾乎相等」運運算元,需要定義新的魔術方法 __almost_eq__
並更新編譯器和解譯器以支援新的運運算元。Python 的 PyObject_RichCompare
函式用於比較兩個物件,並使用 Py_RETURN_RICHCOMPARE
宏執行不同的比較操作。為了支援新的比較運運算元,需要修改 cmp_op
元組、compiler_addcompare
函式、float_richcompare
函式以及 ceval.c
檔案中的相關部分,以確保新的運運算元與現有語言特性相容。
從 Python 呼叫編譯器
編譯器可以透過內建的 compile()
函式從 Python 程式碼中呼叫。該函式傳回程式碼物件:
co = compile("b+1", "test.py", mode="eval")
print(co)
結果:
<code object <module> at 0x10f222780, file "test.py", line 1>
與 symtable()
API 相同,簡單的表示式應使用 “eval” 模式,而模組、函式或類別應使用 “exec” 模式。編譯後的程式碼位於程式碼物件的 co_code
屬性中:
b'e\x00d\x00\x17\x00S\x00'
標準函式庫還包括 dis
模組,可以反組譯位元組碼指令。這些指令可以列印或以 Instruction
物件列表的形式取得。
注意
dis
模組中的 Instruction
型別是 C API 中 instr
型別的反射。如果匯入 dis
模組並將程式碼物件的 co_code
屬性傳遞給 dis()
函式,則該函式會反組譯它並在 REPL 中列印指令:
import dis
dis.dis(co)
結果:
0 LOAD_NAME 0 (0)
2 LOAD_CONST 0 (0)
4 BINARY_ADD
6 RETURN_VALUE
LOAD_NAME
、LOAD_CONST
、BINARY_ADD
和 RETURN_VALUE
是位元組碼指令。術語 “位元組碼” 是因為指令的二進位制形式的長度為 1 個位元組所致。然而,從 Python 3.6 開始,儲存格式已擴充套件到機器詞長,因此正式來說,這是 “詞碼” 而不是 “位元組碼”。
位元組碼指令列表
每個 Python 版本都有一個可用的位元組碼指令列表,並且它們在版本之間有所不同。例如,Python 3.7 引入了新的位元組碼指令,以加速某些方法呼叫的速度。
instaviz
在前面的章節中,我們討論了 instaviz
套件。它的功能包括透過執行編譯器來視覺化程式碼物件型別。此外,instaviz
顯示程式碼物件內的位元組碼操作。
編譯器 C API
編譯模組 AST 的入口點 compiler_mod()
根據模組型別切換不同的編譯器函式。如果模組型別為 Module
,則模組作為編譯單元被編譯到 c_stack
屬性中。然後,assemble()
函式被呼叫以從編譯單元堆積疊建立 PyCodeObject
物件。
傳回新的程式碼物件,它由直譯器執行或快取並儲存在磁碟上的 .pyc
檔案中:
static PyCodeObject *
compiler_mod(struct compiler *c, mod_ty mod)
{
//...
}
這是基本編譯過程的概述,涵蓋了從 Python 呼叫編譯器、編譯過程以及編譯器 C API 的基本知識。
Python 編譯過程
Python 的編譯過程是一個複雜的流程,涉及多個步驟和模組。以下是對編譯過程的概述:
編譯器初始化
編譯器首先初始化一個 compiler
物件,該物件包含了編譯過程所需的資訊和設定。
模組種類別判斷
編譯器根據模組的種類別(Module_kind、Interactive_kind、Expression_kind)進行不同的處理。
編譯主體
對於 Module_kind 模組,編譯器會呼叫 compiler_body
函式來編譯模組的主體。compiler_body
函式會遍歷模組中的所有命令,並呼叫相應的存取函式來處理每個命令。
存取函式
存取函式(例如 compiler_visit_stmt
)會根據命令的種類別進行不同的處理。例如,對於 stmt 命令,編譯器會呼叫 compiler_visit_stmt
函式來處理。
編譯完成
編譯完成後,編譯器會傳回一個 PyCodeObject
物件,該物件包含了編譯後的 bytecode。
程式碼範例
以下是 compiler_body
函式的程式碼範例:
static int
compiler_body(struct compiler *c, asdl_seq *stmts)
{
int i = 0;
stmt_ty st;
PyObject *docstring;
for (; i < asdl_seq_LEN(stmts); i++)
VISIT(c, stmt, (stmt_ty)asdl_seq_GET(stmts, i));
return 1;
}
以下是 compiler_visit_stmt
函式的程式碼範例:
static int
compiler_visit_stmt(struct compiler *c, stmt_ty s)
{
Py_ssize_t i, n;
/* Всегда присваивайте номер строки следующей инструкции для stmt */
SET_LOC(c, s);
switch (s->kind) {
//...
}
}
Mermaid 圖表
以下是編譯過程的 Mermaid 圖表:
graph LR A[編譯器初始化] --> B[模組種類別判斷] B --> C[編譯主體] C --> D[存取函式] D --> E[編譯完成] E --> F[傳回 PyCodeObject]
圖表翻譯:
- 編譯器初始化:初始化編譯器物件
- 模組種類別判斷:根據模組種類別進行不同的處理
- 編譯主體:編譯模組的主體
- 存取函式:根據命令種類別進行不同的處理
- 編譯完成:編譯完成後傳回 PyCodeObject 物件
內容解密:
compiler_body
函式:遍歷模組中的所有命令,並呼叫相應的存取函式來處理每個命令compiler_visit_stmt
函式:根據命令的種類別進行不同的處理- PyCodeObject 物件:包含了編譯後的 bytecode
Python中的for迴圈
Python中的for迴圈是一種用於遍歷可迭代物件(如列表、元組、字典等)的控制結構。其基本語法如下:
for 變數 in 可迭代物件:
# 迴圈體
其中,變數是用於儲存可迭代物件中每個元素的臨時變數,迴圈體則是需要執行的程式碼塊。
for迴圈的工作原理
當Python直譯器遇到一個for迴圈時,它會將可迭代物件轉換為一個迭代器(iterator)。然後,直譯器會在每次迭代中將迭代器的下一個元素指定給變數,並執行迴圈體。
示例
下面是一個簡單的for迴圈示例:
fruits = ['apple', 'banana', 'cherry']
for fruit in fruits:
print(fruit)
這段程式碼會輸出:
apple
banana
cherry
else子句
for迴圈還可以包含一個可選的else子句,當迴圈完成後,else子句將被執行。示例如下:
fruits = ['apple', 'banana', 'cherry']
for fruit in fruits:
print(fruit)
else:
print("迴圈完成")
這段程式碼會輸出:
apple
banana
cherry
迴圈完成
range函式
Python中的range函式可以生成一個可迭代的數字序列,常用於for迴圈中。示例如下:
for i in range(5):
print(i)
這段程式碼會輸出:
0
1
2
3
4
enumerate函式
Python中的enumerate函式可以將一個可迭代物件與一個計數器結合起來,常用於需要取得索引和值的情況。示例如下:
fruits = ['apple', 'banana', 'cherry']
for i, fruit in enumerate(fruits):
print(f"{i}: {fruit}")
這段程式碼會輸出:
0: apple
1: banana
2: cherry
Python中的for迴圈是一種強大的工具,用於遍歷可迭代物件。其基本語法簡單易懂,透過使用range函式和enumerate函式,可以實作更復雜的迴圈邏輯。
Python編譯器的組成和運作
Python編譯器是一個複雜的系統,負責將Python程式碼轉換為機器碼。編譯器的主要目的是將Python程式碼轉換為 bytecode,然後由Python虛擬機器(Python Virtual Machine,PVM)執行。
編譯器的結構
Python編譯器由多個模組組成,每個模組負責不同的任務。編譯器的主要結構包括:
- 詞法分析(Lexical Analysis):負責將Python程式碼分解為個別的token。
- 語法分析(Syntax Analysis):負責根據Python的語法規則,將token組合成抽象語法樹(Abstract Syntax Tree,AST)。
- 語義分析(Semantic Analysis):負責根據Python的語義規則,檢查AST的正確性。
- 中間碼生成(Intermediate Code Generation):負責將AST轉換為中間碼。
- 最佳化(Optimization):負責最佳化中間碼,以提高程式的效率。
- 程式碼生成(Code Generation):負責將中間碼轉換為機器碼。
基本塊(Basic Block)
基本塊是編譯器的一個重要概念。基本塊是一個連續的指令序列,該序列具有以下特點:
- 入口點(Entry Point):基本塊有一個唯一的入口點。
- 出口點(Exit Point):基本塊有一個唯一的出口點。
- 連續性:基本塊中的指令是連續的。
基本塊是編譯器最佳化的基本單位。編譯器可以對基本塊進行最佳化,以提高程式的效率。
指令和引數
編譯器使用指令和引數來表示程式的行為。指令是一個動作,引數是指令的輸入。例如,ADDOP_JREL
是一個指令,表示「相對跳躍」;ADDOP_JABS
是一個指令,表示「絕對跳躍」。
編譯器使用不同的宏來表示不同的指令和引數。例如,ADDOP_I
是一個宏,表示「整數引數」;ADDOP_O
是一個宏,表示「物件引數」。
組裝器(Assembler)
組裝器是編譯器的一部分,負責將中間碼轉換為機器碼。組裝器使用深度優先搜尋(Depth-First Search,DFS)演算法來遍歷基本塊,並將指令序列化為 bytecode。
組裝器的資料結構包括:
b_ialloc
:指令陣列的長度。b_instr
:指令陣列。b_iused
:已使用的指令數量。b_list
:基本塊列表。b_next
:下一個基本塊。b_offset
:指令偏移量。b_return
:傳回值。b_seen
:已存取的基本塊。
組裝器的主要任務是將中間碼轉換為機器碼,並最佳化程式的效率。
Python 編譯器的基礎結構
Python 編譯器是一個複雜的系統,負責將 Python 程式碼轉換為機器碼。編譯器的核心是根據一個稱為「基礎塊」(basic block)的概念。基礎塊是一個連續的指令序列,沒有分支或跳轉。
基礎塊的結構
每個基礎塊都有一個唯一的識別符號(a_bytecode
),它是一個包含了基礎塊中指令的字串。基礎塊還有一個 a_lineno
屬性,表示最後一次生成的指令的行號。另外,基礎塊還有一個 a_lnotab
屬性,它是一個包含了基礎塊中指令的行號和偏移量的字串。
基礎塊的連線
基礎塊之間透過兩種不同的圖形連線。第一種圖形是根據每個基礎塊的 b_list
屬性,這是一個連線到下一個基礎塊的列表。這種圖形用於順序地遍歷所有基礎塊。
第二種圖形是根據每個基礎塊的 b_next
屬性,這是一個連線到下一個基礎塊的指標。這種圖形代表了控制流程,描述了程式的執行順序。
深度優先搜尋(DFS)
編譯器使用深度優先搜尋(DFS)演算法來遍歷基礎塊圖形。DFS 演算法是一種常用的圖形遍歷演算法,它可以有效地遍歷圖形中的所有節點。
基礎塊的應用
基礎塊在 Python 編譯器中扮演著重要的角色。它們用於表示程式的控制流程和指令序列。透過分析基礎塊,可以獲得有關程式結構和行為的重要資訊。
以下是一個示例程式,展示了基礎塊的結構和連線:
import dis
def example():
a = 1
b = 2
if a > b:
print("a is greater than b")
else:
print("a is less than or equal to b")
dis.dis(example)
這個程式定義了一個函式 example()”,它包含了一個 if-else 陳述式。透過使用
dis` 模組,可以將這個函式編譯為機器碼,並檢視其基礎塊的結構和連線。
Python 編譯器的組裝與深度優先搜尋
Python 的編譯器是一個複雜的系統,負責將 Python 程式碼轉換為可以被執行的 bytecode。在這個過程中,編譯器使用了一種叫做深度優先搜尋(DFS)的演算法來處理程式的控制流。
編譯器的 API
Python 的編譯器 API 提供了一個入口點 assemble()”,它負責計算需要分配的記憶體數量、確保每個區塊在邊界之外傳回 None、解析所有相對跳轉指令、呼叫 DFS 函式進行區塊深度優先搜尋、生成所有指令以及呼叫
makecode()` 生成 PyCodeObject。
深度優先搜尋
深度優先搜尋是一種圖形搜尋演算法,Python 的編譯器使用它來遍歷程式的控制流圖。DFS 函式 dfs()
會遞迴地存取每個區塊,將其標記為已存取,並將其新增到反向連結串列中。
區塊連結串列的建立
在 DFS 過程中,編譯器會建立一個區塊連結串列 a_postorder
,其中包含了所有區塊按照反向順序排列。這個連結串列用於生成 bytecode。
跳轉指令的解析
編譯器會解析所有相對跳轉指令,並將其轉換為絕對跳轉指令。
bytecode 的生成
編譯器會根據區塊連結串列生成 bytecode,並將其儲存在 PyCodeObject 中。
程式碼範例
以下是 Python 編譯器中 DFS 函式的程式碼範例:
def dfs(c, b, a, end):
while b and not b.b_seen:
b.b_seen = 1
a.a_postorder[end - 1] = b
b = b.b_next
這個函式會遞迴地存取每個區塊,將其標記為已存取,並將其新增到反向連結串列中。
Python 編譯過程:從CFG到物件碼
Python 的編譯過程是一個複雜的流程,涉及多個階段和模組。下面我們將探討編譯過程中的一個重要階段:從控制流程圖(CFG)到物件碼的生成。
CFG和深度優先搜尋
在編譯過程中,Python 使用深度優先搜尋(DFS)演算法來構建控制流程圖(CFG)。CFG是一個有向圖,描述了程式的控制流程。每個節點代表一個基本塊(basic block),而邊則代表控制流程之間的跳轉。
while (j < end) {
b = a->a_postorder[j++];
for (i = 0; i < b->b_iused; i++) {
struct instr *instr = &b->b_instr[i];
if (instr->i_jrel || instr->i_jabs)
dfs(c, instr->i_target, a, j);
}
assert(a->a_nblocks < j);
a->a_postorder[a->a_nblocks++] = b;
}
物件碼的生成
編譯過程的下一個階段是生成物件碼。物件碼是Python的中間表示形式,描述了程式的控制流程和資料流程。物件碼的生成涉及多個步驟,包括:
- 計算常數和變數:計算常數和變數的值,並將其儲存在物件碼中。
- 計算旗標:計算旗標的值,旗標用於描述程式的屬性,例如是否為生成器函式。
- 最佳化位元組碼:最佳化位元組碼,以減少執行時間和提高效率。
static PyCodeObject *
makecode(struct compiler *c, struct assembler *a)
{
...
consts = consts_dict_keys_inorder(c->u->u_consts);
names = dict_keys_inorder(c->u->u_names, 0);
varnames = dict_keys_inorder(c->u->u_varnames, 0);
...
flags = compute_code_flags(c);
if (flags < 0)
goto error;
bytecode = PyCode_Optimize(a->a_bytecode, consts,
names, a->a_lnotab);
...
co = PyCode_NewWithPosOnlyArgs(
posonlyargcount+posorkeywordargcount,
posonlyargcount, kwonlyargcount, nlocals_int,
maxdepth, flags, bytecode, consts, names,
varnames, freevars, cellvars, c->c_filename,
c->u->u_name, c->u->u_firstlineno, a->a_lnotab);
...
return co;
}
Instaviz模組
Instaviz模組是一個用於視覺化Python物件碼的工具。它可以幫助開發者瞭解程式的控制流程和資料流程,從而更好地最佳化和除錯程式。
import instaviz
def foo():
...
Python 編譯器和運運算元實作
Python 的編譯器是一個複雜的系統,負責將 Python 程式碼轉換為位元組碼(bytecode),然後由 Python 解譯器執行。要了解 Python 的編譯器和運運算元的實作,我們需要深入探討 Python 的內部工作原理。
編譯器和位元組碼
當你執行 Python 程式碼時,Python 編譯器會將程式碼轉換為位元組碼。位元組碼是一種平臺獨立的中間表示形式,可以被 Python 解譯器執行。編譯器會將 Python 程式碼分解為多個步驟,包括語法分析、語義分析和程式碼生成。
運運算元實作
Python 的運運算元是由一組特殊的函式實作的,這些函式被稱為魔術方法(magic methods)。魔術方法是一種特殊的方法,可以被用來定義運運算元的行為。例如,__add__
方法可以被用來定義加法運運算元的行為。
實作「幾乎相等」運運算元
要實作「幾乎相等」運運算元,我們需要定義一個新的魔術方法,例如 __almost_eq__
。然後,我們需要更新 Python 的編譯器和解譯器,以支援這個新的運運算元。
class AlmostEqual:
def __init__(self, value):
self.value = value
def __almost_eq__(self, other):
# 定義「幾乎相等」的行為
return abs(self.value - other.value) < 1e-6
# 測試
a = AlmostEqual(1.0)
b = AlmostEqual(1.000001)
print(a.__almost_eq__(b)) # True
內容解密:
- Python 的編譯器會將 Python 程式碼轉換為位元組碼。
- 位元組碼是一種平臺獨立的中間表示形式,可以被 Python 解譯器執行。
- 運運算元是由一組特殊的函式實作的,這些函式被稱為魔術方法。
- 魔術方法可以被用來定義運運算元的行為。
- 我們可以透過定義新的魔術方法和更新 Python 的編譯器和解譯器來實作新的運運算元。
圖表翻譯:
graph LR A[Python 程式碼] -->|編譯|> B[位元組碼] B -->|執行|> C[Python 解譯器] C -->|運算|> D[魔術方法] D -->|定義|> E[運運算元的行為]
這個圖表展示了 Python 程式碼的編譯和執行過程,以及魔術方法在運運算元實作中的作用。
Python 中的 Rich Compare
Python 的 PyObject_RichCompare
函式用於比較兩個物件。這個函式位於 Objects/object.c
檔案中,負責執行各種比較操作,如相等、不相等、大於、大於或等於、小於、小於或等於等。
Py_RETURN_RICHCOMPARE 宏
Py_RETURN_RICHCOMPARE
宏定義了一個 switch 陳述式,根據運算子 (op
) 的值執行不同的比較操作。運算子的值由 Py_EQ
、Py_NE
、Py_LT
、Py_GT
、Py_LE
和 Py_GE
等常數表示。
#define Py_RETURN_RICHCOMPARE(val1, val2, op) \
do { \
switch (op) { \
case Py_EQ: if ((val1) == (val2)) Py_RETURN_TRUE; Py_RETURN_FALSE; \
case Py_NE: if ((val1)!= (val2)) Py_RETURN_TRUE; Py_RETURN_FALSE; \
case Py_LT: if ((val1) < (val2)) Py_RETURN_TRUE; Py_RETURN_FALSE; \
case Py_GT: if ((val1) > (val2)) Py_RETURN_TRUE; Py_RETURN_FALSE; \
case Py_LE: if ((val1) <= (val2)) Py_RETURN_TRUE; Py_RETURN_FALSE; \
case Py_GE: if ((val1) >= (val2)) Py_RETURN_TRUE; Py_RETURN_FALSE; \
/* + */ case Py_AlE: if ((val1) == (val2)) Py_RETURN_TRUE; Py_RETURN_FALSE;\
default: \
Py_UNREACHABLE(); \
} \
} while (0)
PyObject_RichCompare 函式
PyObject_RichCompare
函式是 Python 中比較兩個物件的核心函式。它首先取得當前執行緒狀態,然後斷言運算子 (op
) 的值在有效範圍內(從 Py_LT
到 Py_GE
)。
PyObject *
PyObject_RichCompare(PyObject *v, PyObject *w, int op)
{
PyThreadState *tstate = _PyThreadState_GET();
assert(Py_LT <= op && op <= Py_GE);
//...
}
為了支援新的 Py_AlE
運算子,我們需要更新這個斷言陳述式,使其包含 Py_AlE
的值。
assert(Py_LT <= op && op <= Py_AlE);
_Py_SwappedOp 列表
_Py_SwappedOp
列表用於定義可以被交換的運算子。我們需要在這個列表中新增 Py_AlE
。
int _Py_SwappedOp[] = {Py_GT, Py_GE, Py_EQ, Py_NE, Py_LT, Py_LE, Py_AlE};
同時,我們需要在 opstrings
列表中新增新的運算子字串 "~="
。
static const char * const opstrings[] = {"<", "<=", "==", "!=", ">", ">=", "~="};
Lib/opcode.py 檔案
在 Lib/opcode.py
檔案中,我們需要更新比較運算子的元組。
cmp_op = ('<', '<=', '==', '!=', '>', '>=', '~=')
透過這些修改,我們可以在 Python 中支援新的 Py_AlE
運算子,並使其能夠正確地執行比較操作。
新增新的比較運算子
首先,我們需要在cmp_op
元組中新增新的比較運算子~=
。這是透過直接在元組中新增新的字串來實作的。
cmp_op = ('<', '<=', '==', '!=', '>', '>=', '~=')
實作~=
運算子
接下來,我們需要在Python的編譯器中實作~=
運算子。這涉及到修改compiler_addcompare
函式以支援新的比較運算子。
static int compiler_addcompare(struct compiler *c, cmpop_ty op)
{
int cmp;
switch (op) {
//...
case AlE:
cmp = Py_AlE;
break;
}
}
實作float_richcompare
函式
為了支援浮點數的~=
運算子,我們需要修改float_richcompare
函式。這個函式負責比較兩個浮點數。
static PyObject*
float_richcompare(PyObject *v, PyObject *w, int op)
{
//...
case Py_AlE: {
double diff = fabs(i - j);
double rel_tol = 1e-9; // 相對誤差
double abs_tol = 0.1; // 絕對誤差
r = (((diff <= fabs(rel_tol * j)) ||
(diff <= fabs(rel_tol * i))) ||
(diff <= abs_tol));
}
break;
return PyBool_FromLong(r);
}
這個實作使用了一個相對簡單的邏輯來判斷兩個浮點數是否“幾乎相等”。它檢查絕對差是否小於某個相對誤差或絕對誤差。如果滿足這個條件,則認為兩個數“幾乎相等”。
修改ceval.c
最後,我們可能需要修改ceval.c
檔案中的某些部分,以確保迴圈計算正確處理新的比較運算子。然而,這個步驟的具體細節取決於Python版本和實作細節,因此這裡不提供具體的程式碼修改建議。
總的來說,新增一個新的比較運算子到Python中需要修改多個部分,包括語法解析、編譯器和執行時函式庫。這些修改需要謹慎考慮,以確保新的運算子與現有的語言特性相容,並且在所有情況下都能正確工作。
從技術架構視角來看,深入剖析 Python 呼叫編譯器的機制,可以發現它是一個精妙的多階段流程,涵蓋詞法分析、語法分析、語義分析、中間碼生成、最佳化以及最終的機器碼生成。本文詳細闡述了編譯器如何利用基礎塊、深度優先搜尋等技術,將 Python 程式碼轉換成可執行的 bytecode。其中,基礎塊作為程式碼的最小執行單元,以及深度優先搜尋在構建控制流程圖和解析跳轉指令中的關鍵作用,都值得深入探討。此外,文章也揭示了 Python 如何透過魔術方法實作運運算元,並以「幾乎相等」運運算元為例,展示了擴充套件 Python 運運算元支援的可能性,包含修改 float_richcompare
函式以及在 cmp_op
元組中加入新的運算子。然而,直接修改 Python 核心來新增新的比較運算子,如文中提到的 ~=
, 存在一定的風險和維護成本。對於追求客製化比較邏輯的開發者,建議優先考慮在應用層級透過函式或自定義類別來實作,以兼顧彈性和可維護性。未來,隨著 Python 的持續發展,預計編譯器將在效能最佳化和新語言特性支援方面持續精進,例如更細粒度的 bytecode 最佳化和更便捷的運運算元擴充套件機制。對於 Python 開發者而言,深入理解編譯器的運作機制,將有助於編寫更高效、更優雅的程式碼。