CPython 作為 Python 的主要直譯器,其執行迴圈是理解 Python 程式運作的關鍵。Python 程式碼首先會被編譯成位元組碼,接著由 CPython 的虛擬機器執行。執行過程中,每個函式呼叫都會建立一個堆積疊框架,用於儲存區域性變數、引數等資訊,並由執行迴圈逐一執行位元組碼指令,管理變數的生命週期。理解堆積疊框架和執行迴圈的互動,有助於我們更深入地理解 Python 的運作方式,進而編寫更高效的程式碼。

執行摘要

在本章中,我們將探討CPython的執行過程,特別是如何將Python程式碼轉換為位元組碼並執行。首先,我們需要了解Python程式碼的編譯過程,包括語法分析、抽象語法樹(AST)生成、位元組碼生成等步驟。

編譯過程

  1. 語法分析:Python程式碼首先被語法分析器解析成一個抽象語法樹(AST)。
  2. AST生成:然後,AST被轉換成位元組碼。
  3. 位元組碼生成:位元組碼是Python虛擬機器可以直接執行的指令集。

執行過程

  1. 執行迴圈:CPython的執行迴圈負責執行位元組碼。它會逐一執行每個位元組碼指令。
  2. 堆積疊框架:每個函式呼叫都會建立一個新的堆積疊框架,用於儲存區域性變數、引數等資訊。
  3. 變數管理:堆積疊框架管理變數的建立、修改和使用。

重要術語

  • 堆積疊框架:是一種資料結構,用於儲存函式呼叫相關的資訊,如區域性變數、引數等。
  • 執行迴圈:是CPython的核心執行機制,負責執行位元組碼。

Python 執行迴圈

Python 的執行迴圈是指 Python 解譯器如何執行 Python 程式碼的過程。這個過程涉及到多個步驟,包括編譯、解譯、執行等。

執行迴圈的步驟

  1. 編譯: Python 程式碼首先被編譯成位元組碼(bytecode)。這個過程是由 Python 解譯器完成的。
  2. 解譯: 位元組碼被解譯成機器碼(machine code)。這個過程是由 Python 解譯器完成的。
  3. 執行: 機器碼被執行。這個過程是由 CPU 完成的。

執行迴圈的結構

Python 的執行迴圈可以分為以下幾個部分:

  1. Global Interpreter Lock (GIL): GIL 是 Python 解譯器用來同步多執行緒存取分享資源的機制。
  2. Thread State: Thread State 是 Python 解譯器用來儲存每個執行緒的狀態的結構。
  3. Frame Object: Frame Object 是 Python 解譯器用來儲存每個函式呼叫的狀態的結構。

執行迴圈的實作

Python 的執行迴圈是由以下幾個檔案實作的:

  1. ceval.c: 這個檔案包含了 Python 解譯器的核心實作。
  2. ceval-gil.h: 這個檔案包含了 GIL 的定義和實作。
  3. thread.c: 這個檔案包含了 Python 解譯器的執行緒管理實作。
  4. threadstate.h: 這個檔案包含了 Thread State 的定義和實作。

執行迴圈的重要概念

  1. Cycle: Cycle 是指 Python 解譯器執行的一個迴圈。
  2. Thread: Thread 是指 Python 解譯器的一個執行緒。
  3. Frame: Frame 是指 Python 解譯器的一個函式呼叫。

144 虛擬機器的執行迴圈

在 Python 的虛擬機器中,執行迴圈是指解譯器如何執行 Python 程式碼的過程。這個過程涉及到多個步驟,包括建立執行環境、載入程式碼、執行程式碼等。

執行環境

執行環境是指虛擬機器執行程式碼時所需的所有資源和設定。這包括了全域變數、區域性變數、堆積疊等。

欄位名稱型別說明
f_codePyCodeObject*程式碼物件,包含了要執行的程式碼
f_executingchar執行標誌,指示是否正在執行程式碼
f_genPyObject*生成器物件,指向生成器函式
f_globalsPyObject* (dict)全域變數表
f_localsPyObject* (dict)區域性變數表
f_iblockint堆積疊索引
f_lastiint上一次執行的指令索引
f_linenoint當前行號
f_tracePyObject*追蹤函式,用於除錯
f_trace_lineschar追蹤行號標誌
f_trace_opcodeschar追蹤操作碼標誌
f_valuestackPyObject**值堆積疊指標

執行迴圈

執行迴圈是指虛擬機器如何執行程式碼的過程。這個過程涉及到多個步驟,包括:

  1. 建立執行環境:虛擬機器建立執行環境,包括載入程式碼、建立全域變數表和區域性變數表等。
  2. 載入程式碼:虛擬機器載入要執行的程式碼。
  3. 執行程式碼:虛擬機器執行程式碼,包括解譯指令、運算等。
  4. 處理異常:虛擬機器處理異常,包括捕捉異常、丟擲異常等。

相關檔案

  • 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() 來存取。建立新的幀物件的過程如下:

  1. 將當前執行緒的最後一個幀指定給 f_back 屬性。
  2. 載入當前的內建函式並將其指定給 f_builtins 屬性。
  3. 將可執行程式碼物件指定給 f_code 屬性。
  4. 建立一個空的值堆疊並將其指定給 f_valuestack 屬性。
  5. f_stacktop 指標指向 f_valuestack
  6. 將全域性變數指定給 f_globals 屬性。
  7. 建立一個新的區域性變數字典並將其指定給 f_locals 屬性。
  8. 將行號指定給 co_firstlineno 屬性,以便於追蹤。
  9. 將所有其他屬性設定為預設值。

構建幀物件

建立新的幀物件後,可以構建幀物件的引數。這些引數包括:

  • 前一個幀
  • 指令
  • 名稱
  • 常數
  • 區域性變數
  • 全域性變數
  • 內建函式
  • 程式碼物件
  • 名稱引數轉換為字典

名稱引數轉換為字典

函式定義可以包含 **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 會進行以下步驟:

  1. 查詢函式物件:Python 會在當前作用域中查詢函式物件,如果找不到,則會在外層作用域中繼續查詢。
  2. 檢查引數:Python 會檢查呼叫函式時傳遞的引數是否正確,包括引數的數量、型別和順序。
  3. 建立呼叫堆積疊:Python 會建立一個呼叫堆積疊,包含了函式的引數、區域性變數和傳回值。
  4. 執行函式:Python 會執行函式的程式碼,包括評估表示式、執行陳述式和呼叫其他函式。

函式執行

在執行函式的過程中,Python 會進行以下步驟:

  1. 建立區域性作用域:Python 會建立一個區域性作用域,包含了函式的區域性變數和引數。
  2. 評估表示式:Python 會評估函式中的表示式,包括算術運算、比較運算和邏輯運算。
  3. 執行陳述式:Python 會執行函式中的陳述式,包括指定陳述式、控制流陳述式和函式呼叫陳述式。
  4. 呼叫其他函式:Python 會呼叫其他函式,包括內建函式和使用者定義的函式。

函式傳回

當函式執行完成後,Python 會進行以下步驟:

  1. 傳回值:Python 會傳回函式的傳回值,如果沒有明確指定傳回值,則傳回 None
  2. 銷毀區域性作用域:Python 會銷毀函式的區域性作用域,包含了區域性變數和引數。
  3. 還原呼叫堆積疊:Python 會還原呼叫堆積疊,包含了外層函式的引數和區域性變數。

例子

以下是個簡單的例子:

def add(a, b):
    return a + b

result = add(2, 3)
print(result)  # 輸出:5

在這個例子中,add 函式被定義為接受兩個引數 ab,並傳回兩個引數的和。當呼叫 add 函式時,Python 會建立一個呼叫堆積疊,包含了引數 23,然後執行 add 函式的程式碼,最後傳回結果 5

Python 執行框架:瞭解 CPython 的核心

Python 的執行框架是其運作的核心,負責將 Python 程式碼轉換為機器碼並執行。這個框架是由 CPython 實作的,CPython 是 Python 的原始實作,也是最廣泛使用的 Python 解譯器。

執行框架的組成

CPython 的執行框架由多個部分組成,包括:

  • Frame 物件:代表了一個執行中的函式或方法呼叫。每個 Frame 物件都包含了當前執行點的資訊,例如當前執行的程式碼、區域性變數等。
  • 程式碼物件:代表了一段 Python 程式碼。程式碼物件包含了程式碼的位元組碼、常數池等資訊。
  • 全域物件:代表了 Python 的全網域名稱空間。全域物件包含了所有的全域變數、函式等。

執行流程

當 Python 程式碼被執行時,CPython 會按照以下流程進行:

  1. 編譯:Python 程式碼被編譯為位元組碼。這個過程是由 compile() 函式完成的。
  2. 建立 Frame 物件:當一個函式或方法被呼叫時,CPython 會建立一個新的 Frame 物件來代表這個呼叫。
  3. 執行:CPython 會執行 Frame 物件中的程式碼。這個過程是由 PyEval_EvalFrame() 函式完成的。
  4. 傳回:當一個函式或方法傳回時,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)

這個範例計算了 ab 的邏輯或運算,並列印預出結果。

堆積疊值模擬

堆積疊值可以透過一個列表來模擬。以下是模擬堆積疊值的範例:

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)中,以便於存取和操作。堆積疊是一種後進先出的資料結構,意味著最近新增的變數會被放在堆積疊的頂部。

變數儲存

當我們定義一個變數時,它會被儲存在堆積疊中。例如,如果我們定義兩個變數 ab,它們會被儲存在堆積疊中,且 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程式碼。