本文 AI 產出,尚未審核

Python 進階主題與實務應用:AST、inspect 與 sys 內部機制


簡介

在日常開發中,我們大多只會使用 Python 的高階 API(如 list.sort()requests.get() 等),卻很少接觸到語言本身的底層結構。
當需要自動化程式碼產生靜態分析動態調試時,astinspectsys 這三個模組就會成為不可或缺的工具。

  • ast(Abstract Syntax Tree)讓我們可以把程式碼當作資料結構來操作,從而實作自訂的 lint、程式碼轉換或迷你編譯器。
  • inspect 提供了「檢視」物件的能力,能夠在執行時取得函式簽名、來源碼、呼叫堆疊等資訊,對除錯與自動化文件產生非常有幫助。
  • sys 則是 Python 執行環境的入口,裡面的屬性(如 sys.modulessys.pathsys._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 會顯示每個節點的類型與屬性,從 ModuleFunctionDefExprCall …,讓我們一目了然程式的結構。

範例 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.versionsys.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")

最佳實踐

  1. 保持可讀性:即使使用 AST 變形,也建議在變形前後使用 ast.unparse(Python 3.9+)或 astor 產生可讀的程式碼,以方便除錯。
  2. 封裝重複邏輯:把常用的 NodeVisitor / NodeTransformer 抽成基礎類別,減少程式碼重複。
  3. 使用型別註解:配合 inspect.signature 可在執行時檢查參數型別,提升 API 的安全性。
  4. 限制 sys._getframe 的深度:除錯時僅抓取必要的堆疊層級,避免不必要的效能開銷。
  5. 模組快取管理:若需要重新載入已修改的模組,使用 importlib.reload(sys.modules[name]),而非直接刪除 sys.modules

實際應用場景

場景 使用模組 為何適合
自動化程式碼轉換(例如把舊式 % 格式化改成 str.format ast + ast.NodeTransformer 可遍歷整個抽象語法樹,安全且不會受字串匹配的誤判影響。
動態產生 API 文件(FastAPI、Flask) inspect.signature + inspect.getdoc 能即時取得函式簽名與說明,配合 Swagger UI 自動生成文件。
插件化 CLI 工具(如 pytestclick sys.pathimportlibsys.modules 允許使用者在任意目錄放置自訂指令,程式在執行時即時載入。
高階除錯與性能分析 inspect.stack()sys._getframe 取得呼叫堆疊與局部變數快照,快速定位錯誤根源。
安全沙箱(限制使用者提交的程式碼) ast.parse + ast.NodeVisitor 在執行前檢查是否含有危險節點(如 Import, Exec, Eval),降低執行惡意程式碼的風險。

範例:簡易安全沙箱
只允許算術運算與變數賦值,不允許 importexeceval

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.pathsys.modulessys._getframe 等屬性,我們可以動態管理模組、取得執行上下文,甚至打造插件化架構。

掌握這三個模組不只是「寫出程式」的技巧,更是 理解 Python 本質提升開發效率打造可維護系統的關鍵。建議讀者在日常開發中逐步嘗試上述範例,從簡單的 AST 走訪、inspect 取得函式資訊,到最後的插件化與安全沙箱實作,逐層深化對 Python 內部機制的認識。祝你在程式碼的世界裡玩得開心、寫得更好!