為什么要了解底層原理
-
寫出高質量代碼
-
問題定位
-
滿足好奇心
-
機械通感
開始
當我們編寫并運行一行?print('Hello, World!')
?時,背后究竟發生了什么?Python 代碼是如何從我們可讀的文本,變成計算機可以執行的指令的呢?
很多人將 Python 稱為一門“解釋型語言”,但這其實不完全準確。更精確地說,Python 是一門先編譯后解釋的語言,這點與java其實非常相似。
它的運行過程涉及兩個核心階段:
-
編譯階段:Python 解釋器首先將我們編寫的源代碼(
.py
?文件)編譯成一種稱為字節碼 (Bytecode)?的中間形式。 -
解釋階段:編譯產生的字節碼隨后由?Python 虛擬機 (Python Virtual Machine, PVM)?來解釋執行。
Part.1 從 Python 源代碼到字節碼
Python 代碼的生命周期始于我們編寫的?.py
?文件。當我們執行一個 Python 腳本時,解釋器并不直接逐行解析這些文本。相反,它會先進行一個“預處理”步驟——編譯成字節碼。
什么是字節碼?
字節碼是一種低級、與平臺無關的指令集。它不像機器碼那樣是給物理 CPU 執行的,而是專門為 Python 虛擬機 (PVM) 設計的。這個設計使得 Python 代碼擁有了出色的可移植性——同一份 Python 代碼可以在任何安裝了 Python 解釋器的操作系統上運行,因為字節碼是標準化的。
這個過程主要分為兩個核心步驟:詞法分析 (Lexical Analysis)?和?語法分析 (Parsing)。可以將它類比為我們閱讀句子的過程:首先識別出每個單詞(詞法分析),然后理解整個句子的語法結構(語法分析)。
核心流程:
源代碼 -> [詞法分析器] -> 令牌流 -> [語法分析器] -> 抽象語法樹 (AST)->字節碼
第 1 步:詞法分析 (Lexical Analysis) - 將代碼分解為“token”
當 Python 解釋器拿到您的源代碼(一串純文本字符)時,它做的第一件事不是去理解代碼的邏輯,而是先將其打碎成一個個有意義的最小單元。這些單元被稱為 “令牌” (Token)。
一個令牌通常包含兩部分信息:
類型:例如 NAME (名稱/標識符), NUMBER (數字), OP (操作符), KEYWORD (關鍵字)。
值:這個令牌對應的具體文本,例如 result, 10, +。
舉個例子,對于我們之前用的代碼 result = a + b,詞法分析器會掃描這段文本,并生成類似下面這樣的一個“令牌流”:
NAME: 'result'
OP: '='
NAME: 'a'
OP: '+'
NAME: 'b'
您可以把這個階段看作是給代碼“斷詞”。Python 內置的 tokenize 模塊可以讓我們親眼看到這個過程:
import tokenize
from io import BytesIOcode = """
result=a+b
"""tokens = tokenize.tokenize(BytesIO(code.encode('utf-8')).readline)for token in tokens:print(token)
結果
TokenInfo(type=63 (ENCODING), string='utf-8', start=(0, 0), end=(0, 0), line='')
TokenInfo(type=62 (NL), string='\n', start=(1, 0), end=(1, 1), line='\n')
TokenInfo(type=1 (NAME), string='result', start=(2, 0), end=(2, 6), line='result=a+b\n')
TokenInfo(type=54 (OP), string='=', start=(2, 6), end=(2, 7), line='result=a+b\n')
TokenInfo(type=1 (NAME), string='a', start=(2, 7), end=(2, 8), line='result=a+b\n')
TokenInfo(type=54 (OP), string='+', start=(2, 8), end=(2, 9), line='result=a+b\n')
TokenInfo(type=1 (NAME), string='b', start=(2, 9), end=(2, 10), line='result=a+b\n')
TokenInfo(type=4 (NEWLINE), string='\n', start=(2, 10), end=(2, 11), line='result=a+b\n')
TokenInfo(type=0 (ENDMARKER), string='', start=(3, 0), end=(3, 0), line='')
類型名 | 描述 |
| 變量名、關鍵字等 |
| 數值 |
| 字符串 |
| 運算符 |
| 縮進 |
| 取消縮進 |
| 語句結束 |
| 文件編碼(如 utf-8) |
| 注釋 |
| 非邏輯新行 |
| 文件結束標記 |
第2步:語法分析
現在我們有了一個扁平的、線性的令牌列表,但解釋器還不知道它們之間的關系。這時 解析器 (Parser) 就登場了。
解析器的任務是接收詞法分析器生成的令牌流,并根據 Python 語言預設的語法規則 (Grammar),將這些令牌組織成一個具有層級結構的樹。這個樹就是抽象語法樹 (AST)。
繼續我們的例子 result = a + b:
解析器拿到 NAME, OP('='), NAME, OP('+'), NAME 這個令牌序列。
它根據 Python 的語法規則,識別出這是一個“賦值語句”的模式。
于是它創建一個 Assign (賦值) 節點作為根。
語法規定,= 左邊的是賦值目標 (target),于是它把第一個 NAME('result') 令牌作為 Assign 節點的 targets 子節點。
= 右邊的 a + b 是要賦的值 (value)。解析器接著分析 NAME('a'), OP('+'), NAME('b') 這部分。
它識別出這是一個“二元運算” (Binary Operation) 的模式。
于是它創建一個 BinOp 節點,并將其作為 Assign 節點的 value 子節點。
語法規定,+ 是操作符 (op),a 是左操作數 (left),b 是右操作數 (right)。解析器相應地創建 Add、Name('a') 和 Name('b') 節點,并將它們作為 BinOp 節點的子節點。
經過這一系列操作,一個扁平的令牌流就變成了一個能清晰反映代碼邏輯和結構的樹狀數據結構——AST。這個 AST 完整地表達了“將變量 a 和 b 相加的結果,賦值給變量 result”這一操作,并且已經去除了所有不影響語法的細節(如空格)。 將 AST(抽象語法樹)轉化為字節碼的過程,是由?Python 的編譯器 (Compiler)?完成的。這個過程可以被理解為一個**“翻譯”**工作:編譯器讀取結構化的 AST,并將其翻譯成一個線性的、供 Python 虛擬機 (PVM) 執行的指令列表。
這個“翻譯”過程的核心思想是?“樹的遍歷” (Tree Traversal)。
第3步:遍歷 AST 并生成指令
編譯器會從 AST 的根節點開始,以深度優先的方式遞歸地訪問(或稱“遍歷”)樹中的每一個節點。對于每一種類型的節點(如?Assign
,?BinOp
,?Name
?等),編譯器都有一個專門的處理函數。這個設計模式通常被稱為?訪問者模式 (Visitor Pattern)。
讓我們以我們熟悉的?result = a + b
?為例,看看編譯器是如何“走”過它的 AST 并生成字節碼的。
import ast# 我們要分析的簡單代碼行
code_line = """
# 這是一個我們編寫的簡單函數
result = a + b
"""# 解析代碼并獲取 AST
tree = ast.parse(code_line)# ast.dump() 可以用一種開發者友好的方式打印出樹的結構
print(ast.dump(tree, indent=4))
這是它的 AST 結構:
Assign(targets=[Name(id='result')],value=BinOp(left=Name(id='a'),op=Add(),right=Name(id='b'))
)
編譯器的工作流程如下:
-
訪問?
Assign
?(賦值) 節點-
編譯器的規則是:要完成一次賦值,必須先計算出右邊的值。
-
因此,它不會立即生成賦值指令,而是遞歸地去訪問?
Assign
?節點的?value
?子節點,也就是?BinOp
?節點。
-
-
訪問?
BinOp
?(二元運算) 節點-
編譯器的規則是:要計算一個二元運算,必須先得到左右兩個操作數的值。
-
它首先遞歸地訪問?
left
?子節點,也就是?Name(id='a')
。
-
-
訪問?
Name(id='a')
?節點-
編譯器看到這是一個變量名,并且當前上下文是需要“加載”它的值。
-
于是,它生成第一條字節碼指令:
LOAD_FAST a
。這條指令的作用是:在程序運行時,從本地變量中找到?a
?的值,并將其推到 PVM 的操作數堆棧頂部。 -
a
?節點處理完畢,返回到?BinOp
?節點的訪問流程。
-
-
回到?
BinOp
?節點-
左邊已經處理完。現在,編譯器遞歸地訪問?
right
?子節點,也就是?Name(id='b')
。
-
-
訪問?
Name(id='b')
?節點-
和處理?
a
?一樣,編譯器生成第二條字節碼指令:LOAD_FAST b
。運行時,b
?的值會被推到堆棧頂部。 -
b
?節點處理完畢,返回到?BinOp
?節點的訪問流程。
-
-
再次回到?
BinOp
?節點-
現在,左右兩個子節點都處理完了。編譯器知道,在運行時,
a
?和?b
?的值會依次位于操作數堆棧上。 -
它查看操作符?
op
?是?Add()
。 -
于是,它生成第三條字節碼指令:
BINARY_ADD
。這條指令會從堆棧彈出頂部的兩個值,將它們相加,然后把結果再推回堆棧頂部。 -
BinOp
?節點處理完畢,返回到?Assign
?節點的訪問流程。
-
-
再次回到?
Assign
?節點-
現在,
value
?子節點(即?a + b
)已經完全處理完。編譯器知道,在運行時,a + b
?的計算結果正位于操作數堆棧的頂部。 -
它查看賦值目標?
targets
?是?Name(id='result')
。 -
于是,它生成第四條字節碼指令:
STORE_FAST result
。這條指令會從堆棧頂部彈出那個計算結果,并將其存入名為?result
?的本地變量中。 -
Assign
?節點處理完畢。
-
至此,result = a + b
?這行代碼的 AST 就被完整地遍歷并轉換成了字節碼序列:
LOAD_FAST a
LOAD_FAST b
BINARY_ADD
STORE_FAST result
Part.2 解釋執行
當字節碼準備就緒后,就輪到 Python 虛擬機 (PVM) 登場了。PVM 是 Python 的運行時引擎,是真正執行代碼的地方。它是一個在您的操作系統上運行的軟件程序,它模擬了一臺“理想”的計算機,專門用來執行 Python 字節碼。
PVM 的核心可以理解為一個巨大的?switch
?循環,我們稱之為評估循環 (Evaluation Loop)?或解釋器循環。這個循環不斷地做三件事:
-
讀取下一條字節碼指令。
-
根據指令的類型,執行相應的操作。
-
重復此過程,直到沒有指令為止。
PVM 是一個堆棧機 (Stack-based machine)。這意味著它使用一種叫做“調用堆棧 (Call Stack)”的數據結構來管理所有的數據和操作。當一個函數被調用時,PVM 會為這個函數創建一個新的幀 (Frame),并將其推入調用堆棧的頂部。
一個幀包含了執行該函數所需的所有信息,主要包括:
-
本地變量 (Local variables): 存儲函數內部定義的變量,如?
a
、b
?和?result
。 -
操作數堆棧 (Operand Stack): 也叫評估堆棧 (Evaluation Stack),用于執行字節碼指令的臨時工作區。例如?
BINARY_ADD
?就是在這個堆棧上完成計算的。 -
指令指針 (Instruction Pointer): 指向當前正在執行的字節碼指令。
-
返回地址: 指向調用該函數的代碼位置,以便函數執行完畢后可以返回。
CPython解釋器核心源碼
https://github.com/python/cpython/blob/3.9/Python/ceval.c
? ? ? ?case TARGET(LOAD_FAST): {
? ? ? ? ? ? PyObject *value = GETLOCAL(oparg);
? ? ? ? ? ? if (value == NULL) {
? ? ? ? ? ? ? ? format_exc_check_arg(tstate, PyExc_UnboundLocalError,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?UNBOUNDLOCAL_ERROR_MSG,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?PyTuple_GetItem(co->co_varnames, oparg));
? ? ? ? ? ? ? ? goto error;
? ? ? ? ? ? }
? ? ? ? ? ? Py_INCREF(value);
? ? ? ? ? ? PUSH(value);
? ? ? ? ? ? FAST_DISPATCH();
? ? ? ? }
? ? ? ? case TARGET(LOAD_CONST): {
? ? ? ? ? ? PREDICTED(LOAD_CONST);
? ? ? ? ? ? PyObject *value = GETITEM(consts, oparg);
? ? ? ? ? ? Py_INCREF(value);
? ? ? ? ? ? PUSH(value);
? ? ? ? ? ? FAST_DISPATCH();
? ? ? ? }
? ? ? ? case TARGET(STORE_FAST): {
? ? ? ? ? ? PREDICTED(STORE_FAST);
? ? ? ? ? ? PyObject *value = POP();
? ? ? ? ? ? SETLOCAL(oparg, value);
? ? ? ? ? ? FAST_DISPATCH();
? ? ? ? }
留個坑
GIL、JIT、GC