Python 進階主題與實務應用:AST、inspect 與 sys 內部機制
簡介
在日常開發中,我們大多只會使用 Python 的高階 API(如 list.sort()、requests.get() 等),卻很少接觸到語言本身的底層結構。
當需要自動化程式碼產生、靜態分析或動態調試時,ast、inspect 與 sys 這三個模組就會成為不可或缺的工具。
ast(Abstract Syntax Tree)讓我們可以把程式碼當作資料結構來操作,從而實作自訂的 lint、程式碼轉換或迷你編譯器。inspect提供了「檢視」物件的能力,能夠在執行時取得函式簽名、來源碼、呼叫堆疊等資訊,對除錯與自動化文件產生非常有幫助。sys則是 Python 執行環境的入口,裡面的屬性(如sys.modules、sys.path、sys._getframe)讓我們得以操控與觀測直譯器的行為。
本篇文章將以 淺顯易懂 的方式,說明這三個模組的核心概念、常見陷阱與實務應用,並提供完整的程式碼範例,幫助初學者與中階開發者快速上手。
核心概念
1. ast:把程式碼當作資料結構
Python 會先把原始程式碼 解析成抽象語法樹 (AST),再由這棵樹產生 Bytecode。透過 ast 模組,我們可以:
| 功能 | 說明 |
|---|---|
ast.parse(source) |
把字串轉成 AST 節點(根為 Module) |
ast.dump(node, annotate_fields=True, include_attributes=False) |
以易讀的文字形式顯示 AST 結構 |
ast.NodeVisitor |
走訪(visit)AST,適合分析 |
ast.NodeTransformer |
變形(transform)AST,適合改寫程式碼 |
compile(node, filename, mode) |
把 AST 重新編譯成可執行的 Code object |
範例 1:解析簡單程式碼並列印 AST
import ast
source = """
def greet(name):
print(f"Hello, {name}!")
"""
# 解析成 AST
tree = ast.parse(source)
# 以文字方式顯示
print(ast.dump(tree, indent=4))
說明:
ast.dump會顯示每個節點的類型與屬性,從Module→FunctionDef→Expr→Call…,讓我們一目了然程式的結構。
範例 2:使用 NodeVisitor 取得所有函式名稱
class FuncNameVisitor(ast.NodeVisitor):
def __init__(self):
self.names = []
def visit_FunctionDef(self, node):
self.names.append(node.name) # 收集函式名稱
self.generic_visit(node) # 繼續往下走訪
visitor = FuncNameVisitor()
visitor.visit(tree)
print("函式名稱:", visitor.names)
說明:只要實作
visit_節點類型方法,就能在遍歷過程中插入自己的邏輯。
範例 3:使用 NodeTransformer 為所有 print 加上前綴
class PrintPrefixTransformer(ast.NodeTransformer):
def visit_Call(self, node):
# 判斷是否是 print(...)
if isinstance(node.func, ast.Name) and node.func.id == "print":
# 把原本的 print 改成自訂的 logger.print
node.func = ast.Attribute(value=ast.Name(id="logger", ctx=ast.Load()),
attr="print",
ctx=ast.Load())
return self.generic_visit(node)
new_tree = PrintPrefixTransformer().visit(tree)
codeobj = compile(new_tree, filename="<ast>", mode="exec")
exec(codeobj, {"logger": __import__("logging").getLogger(__name__)})
說明:
NodeTransformer會回傳 新的 節點(或原節點),最後再compile成可執行的程式碼。這種技巧常被用於自動加上日誌、型別檢查或安全性檢查。
2. inspect:在執行時「觀察」Python 物件
inspect 能夠取得函式、類別、模組等物件的元資訊。以下是最常用的 API:
| API | 功能 |
|---|---|
inspect.getsource(obj) |
取得原始來源碼(需在同一檔案或可取得的模組) |
inspect.getfile(obj) |
取得定義所在檔案路徑 |
inspect.signature(callable) |
取得函式簽名(參數名稱、預設值、型別註解) |
inspect.stack()、inspect.currentframe() |
取得呼叫堆疊與當前執行框架 |
inspect.isgenerator(obj)、inspect.iscoroutinefunction(obj) |
判斷物件類型 |
範例 4:自動產生函式說明文件
import inspect
import textwrap
def docstring_from_signature(func):
"""根據函式簽名自動產生簡易說明文件"""
sig = inspect.signature(func)
params = ", ".join(str(p) for p in sig.parameters.values())
return f"{func.__name__}({params})\n\n{inspect.getdoc(func) or ''}"
def sample(a, b=10, *args, **kwargs):
"""計算 a 與 b 的總和,並回傳額外參數的長度。"""
return a + b, len(args), len(kwargs)
print(docstring_from_signature(sample))
說明:
inspect.signature讓我們可以 程式化 產生文件,適合自動化 API 文件生成或測試框架的 stub 建立。
範例 5:利用 inspect.stack() 取得呼叫者資訊(簡易除錯)
import inspect
def debug_log(message):
# 取得上一層的框架資訊
frame = inspect.stack()[1]
filename = frame.filename
lineno = frame.lineno
funcname = frame.function
print(f"[DEBUG] {filename}:{lineno} ({funcname}) - {message}")
def foo():
debug_log("進入 foo")
foo()
說明:
inspect.stack()會回傳一個FrameInfo列表,索引 0 為當前函式本身,索引 1 為呼叫者。此技巧常在 自訂 logger、測試斷言 中使用。
3. sys:與 Python 執行環境直接互動
sys 提供了對直譯器最底層的存取,常見屬性包括:
| 屬性/函式 | 功能 |
|---|---|
sys.modules |
已載入的模組字典,手動插入或移除可控制模組快取 |
sys.path |
模組搜尋路徑(list),可在執行時動態加入自訂目錄 |
sys.argv |
程式啟動時的命令列參數 |
sys.version、sys.implementation |
取得 Python 版本與實作資訊 |
sys._getframe(depth=0) |
取得當前或上層的執行框架(低階) |
sys.setrecursionlimit(limit) |
調整遞迴深度上限 |
範例 6:動態載入同目錄下的插件
import sys
import importlib
import pathlib
PLUGIN_DIR = pathlib.Path(__file__).parent / "plugins"
sys.path.insert(0, str(PLUGIN_DIR)) # 把插件目錄放在最前面
def load_plugin(name):
try:
module = importlib.import_module(name)
print(f"成功載入插件: {module.__name__}")
return module
except ImportError as e:
print(f"載入失敗: {e}")
# 假設 plugins/hello.py 存在
plugin = load_plugin("hello")
plugin.run()
說明:透過
sys.path的即時調整,我們可以在不修改環境變數的情況下 熱插拔 插件,常見於 CLI 工具或 Web 框架的擴充機制。
範例 7:利用 sys._getframe 取得局部變數快照(除錯神器)
import sys
def snapshot():
frame = sys._getframe(1) # 取得呼叫者的 frame
locals_snapshot = frame.f_locals.copy()
print("局部變數快照:", locals_snapshot)
def demo():
x = 42
y = "hello"
snapshot()
demo()
說明:
sys._getframe為 非官方 API(以_開頭),在正式產品中使用需謹慎;但在除錯或測試時非常方便。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
AST 節點修改忘記 generic_visit |
只改寫特定節點卻未繼續遍歷,導致子節點保持舊樣式。 | 在 visit_* 或 transform_* 方法最後呼叫 self.generic_visit(node)。 |
inspect.getsource 在 REPL 中失效 |
REPL(如 Jupyter)沒有實體檔案,無法取得來源。 | 使用 inspect.getsource 前先檢查 inspect.getfile 是否為 <stdin>,或改用 dill.source.getsource。 |
sys.path 重複加入路徑 |
多次插入同一路徑會造成搜尋效率下降,且可能載入舊版模組。 | 在插入前檢查 if str(path) not in sys.path:,或使用 sys.path.append 並在程式結束時 pop。 |
sys._getframe 在優化模式 (‑O) 被禁用 |
以 -O 執行時,sys._getframe 仍可用,但某些實作會限制深度。 |
盡量避免在正式發行版使用,改以 inspect.stack() 或自訂 decorator 記錄資訊。 |
AST 產生的 Code object 缺少 __file__ |
直接 exec 編譯的程式碼在錯誤回溯時不會顯示檔案名稱。 |
在 compile 時提供虛擬檔名,例如 compile(node, filename="<generated>", mode="exec")。 |
最佳實踐:
- 保持可讀性:即使使用 AST 變形,也建議在變形前後使用
ast.unparse(Python 3.9+)或astor產生可讀的程式碼,以方便除錯。 - 封裝重複邏輯:把常用的
NodeVisitor/NodeTransformer抽成基礎類別,減少程式碼重複。 - 使用型別註解:配合
inspect.signature可在執行時檢查參數型別,提升 API 的安全性。 - 限制
sys._getframe的深度:除錯時僅抓取必要的堆疊層級,避免不必要的效能開銷。 - 模組快取管理:若需要重新載入已修改的模組,使用
importlib.reload(sys.modules[name]),而非直接刪除sys.modules。
實際應用場景
| 場景 | 使用模組 | 為何適合 |
|---|---|---|
自動化程式碼轉換(例如把舊式 % 格式化改成 str.format) |
ast + ast.NodeTransformer |
可遍歷整個抽象語法樹,安全且不會受字串匹配的誤判影響。 |
| 動態產生 API 文件(FastAPI、Flask) | inspect.signature + inspect.getdoc |
能即時取得函式簽名與說明,配合 Swagger UI 自動生成文件。 |
插件化 CLI 工具(如 pytest、click) |
sys.path、importlib、sys.modules |
允許使用者在任意目錄放置自訂指令,程式在執行時即時載入。 |
| 高階除錯與性能分析 | inspect.stack()、sys._getframe |
取得呼叫堆疊與局部變數快照,快速定位錯誤根源。 |
| 安全沙箱(限制使用者提交的程式碼) | ast.parse + ast.NodeVisitor |
在執行前檢查是否含有危險節點(如 Import, Exec, Eval),降低執行惡意程式碼的風險。 |
範例:簡易安全沙箱
只允許算術運算與變數賦值,不允許import、exec、eval。
class SafeVisitor(ast.NodeVisitor):
ALLOWED_NODES = {
ast.Module, ast.Expr, ast.Assign, ast.Name, ast.Load,
ast.BinOp, ast.Add, ast.Sub, ast.Mult, ast.Div,
ast.Num, ast.Constant, ast.UnaryOp, ast.USub,
}
def generic_visit(self, node):
if type(node) not in self.ALLOWED_NODES:
raise ValueError(f"不允許的語法: {type(node).__name__}")
super().generic_visit(node)
def run_safe(code):
tree = ast.parse(code)
SafeVisitor().visit(tree) # 檢查安全性
compiled = compile(tree, "<sandbox>", "exec")
exec(compiled, {})
run_safe("a = 1 + 2 * 3") # ✅
# run_safe("import os; os.system('rm -rf /')") # ❌ 會拋出 ValueError
這樣的沙箱在 線上判題系統、教育平台或 自訂腳本執行環境中相當實用。
總結
**ast**讓程式碼變成可遍歷與改寫的樹狀結構,適合靜態分析、程式碼轉換與自動化生成。**inspect**提供了在執行時取得函式簽名、來源碼、呼叫堆疊等資訊的能力,對除錯、文件產生與測試框架非常關鍵。**sys**是與 Python 執行環境直接互動的窗口,透過sys.path、sys.modules、sys._getframe等屬性,我們可以動態管理模組、取得執行上下文,甚至打造插件化架構。
掌握這三個模組不只是「寫出程式」的技巧,更是 理解 Python 本質、提升開發效率、打造可維護系統的關鍵。建議讀者在日常開發中逐步嘗試上述範例,從簡單的 AST 走訪、inspect 取得函式資訊,到最後的插件化與安全沙箱實作,逐層深化對 Python 內部機制的認識。祝你在程式碼的世界裡玩得開心、寫得更好!