CPython 作為 Python 的主要直譯器,其執行迴圈是理解 Python 程式運作的關鍵。Python 程式碼首先會被編譯成位元組碼,接著由 CPython 的虛擬機器執行。執行過程中,每個函式呼叫都會建立一個堆積疊框架,用於儲存區域性變數、引數等資訊,並由執行迴圈逐一執行位元組碼指令,管理變數的生命週期。理解堆積疊框架和執行迴圈的互動,有助於我們更深入地理解 Python 的運作方式,進而編寫更高效的程式碼。
執行摘要
在本章中,我們將探討CPython的執行過程,特別是如何將Python程式碼轉換為位元組碼並執行。首先,我們需要了解Python程式碼的編譯過程,包括語法分析、抽象語法樹(AST)生成、位元組碼生成等步驟。
編譯過程
- 語法分析:Python程式碼首先被語法分析器解析成一個抽象語法樹(AST)。
- AST生成:然後,AST被轉換成位元組碼。
- 位元組碼生成:位元組碼是Python虛擬機器可以直接執行的指令集。
執行過程
- 執行迴圈:CPython的執行迴圈負責執行位元組碼。它會逐一執行每個位元組碼指令。
- 堆積疊框架:每個函式呼叫都會建立一個新的堆積疊框架,用於儲存區域性變數、引數等資訊。
- 變數管理:堆積疊框架管理變數的建立、修改和使用。
重要術語
- 堆積疊框架:是一種資料結構,用於儲存函式呼叫相關的資訊,如區域性變數、引數等。
- 執行迴圈:是CPython的核心執行機制,負責執行位元組碼。
Python 執行迴圈
Python 的執行迴圈是指 Python 解譯器如何執行 Python 程式碼的過程。這個過程涉及到多個步驟,包括編譯、解譯、執行等。
執行迴圈的步驟
- 編譯: Python 程式碼首先被編譯成位元組碼(bytecode)。這個過程是由 Python 解譯器完成的。
- 解譯: 位元組碼被解譯成機器碼(machine code)。這個過程是由 Python 解譯器完成的。
- 執行: 機器碼被執行。這個過程是由 CPU 完成的。
執行迴圈的結構
Python 的執行迴圈可以分為以下幾個部分:
- Global Interpreter Lock (GIL): GIL 是 Python 解譯器用來同步多執行緒存取分享資源的機制。
- Thread State: Thread State 是 Python 解譯器用來儲存每個執行緒的狀態的結構。
- Frame Object: Frame Object 是 Python 解譯器用來儲存每個函式呼叫的狀態的結構。
執行迴圈的實作
Python 的執行迴圈是由以下幾個檔案實作的:
ceval.c
: 這個檔案包含了 Python 解譯器的核心實作。ceval-gil.h
: 這個檔案包含了 GIL 的定義和實作。thread.c
: 這個檔案包含了 Python 解譯器的執行緒管理實作。threadstate.h
: 這個檔案包含了 Thread State 的定義和實作。
執行迴圈的重要概念
- Cycle: Cycle 是指 Python 解譯器執行的一個迴圈。
- Thread: Thread 是指 Python 解譯器的一個執行緒。
- Frame: Frame 是指 Python 解譯器的一個函式呼叫。
144 虛擬機器的執行迴圈
在 Python 的虛擬機器中,執行迴圈是指解譯器如何執行 Python 程式碼的過程。這個過程涉及到多個步驟,包括建立執行環境、載入程式碼、執行程式碼等。
執行環境
執行環境是指虛擬機器執行程式碼時所需的所有資源和設定。這包括了全域變數、區域性變數、堆積疊等。
欄位名稱 | 型別 | 說明 |
---|---|---|
f_code | PyCodeObject* | 程式碼物件,包含了要執行的程式碼 |
f_executing | char | 執行標誌,指示是否正在執行程式碼 |
f_gen | PyObject* | 生成器物件,指向生成器函式 |
f_globals | PyObject* (dict) | 全域變數表 |
f_locals | PyObject* (dict) | 區域性變數表 |
f_iblock | int | 堆積疊索引 |
f_lasti | int | 上一次執行的指令索引 |
f_lineno | int | 當前行號 |
f_trace | PyObject* | 追蹤函式,用於除錯 |
f_trace_lines | char | 追蹤行號標誌 |
f_trace_opcodes | char | 追蹤操作碼標誌 |
f_valuestack | PyObject** | 值堆積疊指標 |
執行迴圈
執行迴圈是指虛擬機器如何執行程式碼的過程。這個過程涉及到多個步驟,包括:
- 建立執行環境:虛擬機器建立執行環境,包括載入程式碼、建立全域變數表和區域性變數表等。
- 載入程式碼:虛擬機器載入要執行的程式碼。
- 執行程式碼:虛擬機器執行程式碼,包括解譯指令、運算等。
- 處理異常:虛擬機器處理異常,包括捕捉異常、丟擲異常等。
相關檔案
Objects/frameobject.c
:實作了框架物件和 Python API。Include/frameobject.h
:定義了框架物件的 API 和型別。
建立框架物件
建立框架物件是指建立一個新的框架物件,用於執行程式碼。這個過程涉及到多個步驟,包括建立執行環境、載入程式碼等。
API 和內部實作
API 和內部實作是指虛擬機器如何實作執行迴圈的細節。這包括了 _PyEval_EvalCode()
函式,該函式是執行迴圈的核心。
_PyEval_EvalCode()
函式定義了一組引數,包括:
tstate
:執行緒狀態物件。_co
:程式碼物件。globals
:全域變數表。locals
:區域性變數表。
這個函式負責建立執行環境、載入程式碼、執行程式碼等。它是虛擬機器執行迴圈的核心。
Python 中的區域性和全域性變數
在 Python 中,區域性和全域性變數以字典的形式儲存。可以使用內建函式 locals()
和 globals()
來存取這些字典。
a = 1
print(locals()["a"]) # 輸出:1
建立新的幀物件
_PyFrame_New_NoTrack()
函式建立一個新的幀物件。這個函式也可以透過 C API 使用 PyFrame_New()
來存取。建立新的幀物件的過程如下:
- 將當前執行緒的最後一個幀指定給
f_back
屬性。 - 載入當前的內建函式並將其指定給
f_builtins
屬性。 - 將可執行程式碼物件指定給
f_code
屬性。 - 建立一個空的值堆疊並將其指定給
f_valuestack
屬性。 - 將
f_stacktop
指標指向f_valuestack
。 - 將全域性變數指定給
f_globals
屬性。 - 建立一個新的區域性變數字典並將其指定給
f_locals
屬性。 - 將行號指定給
co_firstlineno
屬性,以便於追蹤。 - 將所有其他屬性設定為預設值。
構建幀物件
建立新的幀物件後,可以構建幀物件的引數。這些引數包括:
- 前一個幀
- 指令
- 名稱
- 常數
- 值
- 區域性變數
- 全域性變數
- 內建函式
- 程式碼物件
- 名稱引數轉換為字典
名稱引數轉換為字典
函式定義可以包含 **kwargs
來接受名稱引數。這些名稱引數可以轉換為字典:
def example(arg, arg2=None, **kwargs):
print(kwargs["x"], kwargs["y"]) # 輸出:2 3
example(1, x=2, y=3)
在這個指令碼中,建立了一個新的字典,並將未轉換的引數複製到其中。然後,在幀的區域性作用域中,建立了一個名為 kwargs
的變數。
位置引數轉換為變數
每個位置引數(如果提供)都被設定為區域性變數。在 Python 中,函式引數已經是函式體中的區域性變數。當位置引數被定義時,它們變得可存取:
def example(arg1, arg2):
print(arg1, arg2)
example(1, 2) # 輸出:1 2
這些變數的參照計數增加,因此垃圾回收器不會在幀計算完成之前刪除它們,例如,當函式完成並傳回結果時。
包裝位置引數為 *args
與 **kwargs
類別似,函式引數可以使用 *
符號來捕捉所有剩餘的位置引數。建立一個名為 *args
的區域性變數,其型別為元組:
def example(arg, *args):
print(args) # 輸出:(2, 3)
example(1, 2, 3)
函式呼叫與執行
在 Python 中,函式的呼叫與執行是一個複雜的過程,涉及多個步驟和機制。下面是對這個過程的詳細解釋。
函式定義
當定義一個函式時,Python 會建立一個函式物件,這個物件包含了函式的程式碼、引數和其他相關資訊。函式物件可以被呼叫,從而執行函式的程式碼。
函式呼叫
當呼叫一個函式時,Python 會進行以下步驟:
- 查詢函式物件:Python 會在當前作用域中查詢函式物件,如果找不到,則會在外層作用域中繼續查詢。
- 檢查引數:Python 會檢查呼叫函式時傳遞的引數是否正確,包括引數的數量、型別和順序。
- 建立呼叫堆積疊:Python 會建立一個呼叫堆積疊,包含了函式的引數、區域性變數和傳回值。
- 執行函式:Python 會執行函式的程式碼,包括評估表示式、執行陳述式和呼叫其他函式。
函式執行
在執行函式的過程中,Python 會進行以下步驟:
- 建立區域性作用域:Python 會建立一個區域性作用域,包含了函式的區域性變數和引數。
- 評估表示式:Python 會評估函式中的表示式,包括算術運算、比較運算和邏輯運算。
- 執行陳述式:Python 會執行函式中的陳述式,包括指定陳述式、控制流陳述式和函式呼叫陳述式。
- 呼叫其他函式:Python 會呼叫其他函式,包括內建函式和使用者定義的函式。
函式傳回
當函式執行完成後,Python 會進行以下步驟:
- 傳回值:Python 會傳回函式的傳回值,如果沒有明確指定傳回值,則傳回
None
。 - 銷毀區域性作用域:Python 會銷毀函式的區域性作用域,包含了區域性變數和引數。
- 還原呼叫堆積疊:Python 會還原呼叫堆積疊,包含了外層函式的引數和區域性變數。
例子
以下是個簡單的例子:
def add(a, b):
return a + b
result = add(2, 3)
print(result) # 輸出:5
在這個例子中,add
函式被定義為接受兩個引數 a
和 b
,並傳回兩個引數的和。當呼叫 add
函式時,Python 會建立一個呼叫堆積疊,包含了引數 2
和 3
,然後執行 add
函式的程式碼,最後傳回結果 5
。
Python 執行框架:瞭解 CPython 的核心
Python 的執行框架是其運作的核心,負責將 Python 程式碼轉換為機器碼並執行。這個框架是由 CPython 實作的,CPython 是 Python 的原始實作,也是最廣泛使用的 Python 解譯器。
執行框架的組成
CPython 的執行框架由多個部分組成,包括:
- Frame 物件:代表了一個執行中的函式或方法呼叫。每個 Frame 物件都包含了當前執行點的資訊,例如當前執行的程式碼、區域性變數等。
- 程式碼物件:代表了一段 Python 程式碼。程式碼物件包含了程式碼的位元組碼、常數池等資訊。
- 全域物件:代表了 Python 的全網域名稱空間。全域物件包含了所有的全域變數、函式等。
執行流程
當 Python 程式碼被執行時,CPython 會按照以下流程進行:
- 編譯:Python 程式碼被編譯為位元組碼。這個過程是由
compile()
函式完成的。 - 建立 Frame 物件:當一個函式或方法被呼叫時,CPython 會建立一個新的 Frame 物件來代表這個呼叫。
- 執行:CPython 會執行 Frame 物件中的程式碼。這個過程是由
PyEval_EvalFrame()
函式完成的。 - 傳回:當一個函式或方法傳回時,CPython 會銷毀相應的 Frame 物件,並傳回結果給呼叫者。
執行框架的最佳化
CPython 的執行框架已經被最佳化了多年,以提高 Python 程式碼的執行效率。其中一些最佳化包括:
- 快取:CPython 使用快取來儲存經常使用的程式碼和資料,以減少查詢和載入的時間。
- 內聯:CPython 可以內聯一些小型函式,以減少函式呼叫的開銷。
- 迴圈最佳化:CPython 可以最佳化一些迴圈,以減少迭代的次數和時間。
跟蹤執行流程
Python 3.7 及以上版本提供了一個 sys.settrace()
函式,可以用來跟蹤執行流程。這個函式可以設定一個全域的跟蹤函式,這個函式會在每個 Frame 物件被建立和銷毀時被呼叫。
以下是一個示例程式碼,展示瞭如何使用 sys.settrace()
跟蹤執行流程:
import sys
def my_trace(frame, event, args):
if event == 'call':
print(f"Calling {frame.f_code.co_name}")
elif event == 'return':
print(f"Returning from {frame.f_code.co_name}")
sys.settrace(my_trace)
def test():
print("Hello, World!")
test()
這個程式碼會輸出:
Calling test
Hello, World!
Returning from test
這展示了 sys.settrace()
如何跟蹤執行流程,並在每個 Frame 物件被建立和銷毀時呼叫跟蹤函式。
Python 執行緒追蹤與堆積疊值
Python 的執行緒追蹤是一個強大的工具,能夠幫助開發者瞭解程式的執行流程。透過使用 sys.settrace()
函式,可以設定一個追蹤函式,該函式會在每個執行步驟中被呼叫。
執行緒追蹤
以下是設定追蹤函式的範例:
import sys
def my_trace(frame, event, arg):
print(f"{frame.f_lineno:04d} {event:10s} {arg}")
sys.settrace(my_trace)
這個範例設定了一個追蹤函式 `my_trace()”,該函式會在每個執行步驟中被呼叫,並列印預出目前的行號、事件型別和引數。
堆積疊值
Python 的堆積疊值是一個列表,儲存著目前的執行環境。堆積疊值可以透過 sys._getframe()
函式獲得。
以下是使用堆積疊值的範例:
import sys
def my_function():
frame = sys._getframe()
print(frame.f_locals)
my_function()
這個範例定義了一個函式 `my_function()”,該函式會列印預出目前的區域性變數。
BINARY_OR 指令
Python 的 BINARY_OR 指令是一個二元運算子,用於計算兩個運算元的邏輯或運算。以下是 BINARY_OR 指令的範例:
a = 10
b = 20
result = a or b
print(result)
這個範例計算了 a
和 b
的邏輯或運算,並列印預出結果。
堆積疊值模擬
堆積疊值可以透過一個列表來模擬。以下是模擬堆積疊值的範例:
stack = []
def push(value):
stack.append(value)
def pop():
return stack.pop()
push(10)
push(20)
print(pop()) # 20
print(pop()) # 10
這個範例定義了一個堆積疊值模擬器,使用一個列表來儲存值。push()
函式新增一個值到堆積疊中,pop()
函式移除並傳回堆積疊中的頂部值。
156 迴圈計算
在 Python 的執行過程中,變數會被儲存在堆積疊(stack)中,以便於存取和操作。堆積疊是一種後進先出的資料結構,意味著最近新增的變數會被放在堆積疊的頂部。
變數儲存
當我們定義一個變數時,它會被儲存在堆積疊中。例如,如果我們定義兩個變數 a
和 b
,它們會被儲存在堆積疊中,且 a
會被放在第二個位置,b
會被放在頂部。
取得堆積疊頂部值
如果我們想要取得堆積疊頂部的值,可以使用 POP()
函式。這個函式會傳回堆積疊頂部的值,並將其從堆積疊中移除。
取得堆積疊中特定位置的值
如果我們想要取得堆積疊中特定位置的值,可以使用 PEEK(v)
函式,其中 v
是位置索引。位置索引從 0 開始,0 代表堆積疊頂部。
複製堆積疊頂部值
如果我們想要複製堆積疊頂部的值,可以使用 DUP_TOP()
函式。這個函式會複製堆積疊頂部的值,並將其放在堆積疊頂部。
交換堆積疊中兩個值
如果我們想要交換堆積疊中兩個值,可以使用 ROT_TWO()
函式。這個函式會交換堆積疊中前兩個值的位置。
從程式碼執行效能最佳化的角度來看,深入理解CPython的執行迴圈至關重要。本篇從Python原始碼編譯到位元組碼、執行迴圈的各個階段、堆積疊框架的運作機制,以及關鍵指令如BINARY_OR對堆積疊的操作等方面,逐步揭示了CPython的內部運作原理。理解這些底層機制,例如變數在堆積疊中的儲存方式、POP()
、PEEK()
等函式的操作,能幫助開發者編寫更高效的Python程式碼。然而,CPython的執行迴圈的複雜性也帶來了效能瓶頸,例如全域性直譯器鎖(GIL)的存在限制了多執行緒的平行效率。未來,CPython的發展方向可能包含對GIL的進一步最佳化,或是探索新的平行執行模型,以提升Python在多核心處理器上的效能。對於追求極致效能的應用場景,開發者需要深入研究CPython的執行機制,並考慮使用Cython、Numba等工具來提升效能。對於大多數應用而言,掌握CPython的基本執行原理,並遵循最佳實務,即可編寫出效能良好的Python程式碼。