攝影或3C

Python 核心技巧:從俄羅斯娃娃看懂「遞迴走訪」與 AST 語法樹

traverse 這樣的遞迴 (Recursion) 結構,
正是用來處理「樹狀結構 (Tree Structure)」的最強大武器。

不管是 JSON 設定檔、資料夾裡的檔案架構,
還是程式語言的 AST (抽象語法樹, Abstract Syntax Tree),
在本質上都是一種樹狀結構:它們都有一個「根節點」,
然後往下分岔出無數個「子節點」,子節點又會繼續往下分岔。

這裡我們用一個簡單的例子,
帶你了解如何用遞迴來「走訪 (Traverse)」這些資料,
並且看看這跟 AST 有什麼關聯。

第一步:理解遞迴的核心精神 —— 「俄羅斯娃娃」
遞迴的核心精神就是「自己呼叫自己」
想像你在玩一個巨大的俄羅斯娃娃,
你的目標是把裡面所有的玩偶都拿出來排好。
你可以把這件事寫成一個規則:

def 打開娃娃(當前的娃娃):
    # 1. 終止條件 (Base Case):如果沒有更小的了就停下來
    if 當前的娃娃.是實心的():
        紀錄("找到最核心的實心娃娃了!")
        return
        
    # 2. 遞迴條件 (Recursive Step):如果裡面還有娃娃
    else:
        # 先從當前的娃娃肚子裡把小娃娃拿出來
        裡面的小娃娃 = 當前的娃娃.取出內容物()  
        
        # 然後針對這個小娃娃再次執行打開娃娃的任務自己呼叫自己
        打開娃娃(裡面的小娃娃) 

第二步:簡化的 JSON 遞迴範例
回到你程式碼裡的 traverse 結構。
假設我們有一個非常簡單的 JSON
(其實在 Python 裡這就是巢狀字典和列表),
我們目標是「找出所有存在 class_name 的積木」

data = {
    "step1": {
        "class_name": "LoginStep",
        "description": "User logs in"
    },
    "step2": [
        {
            "class_name": "VerificationStep",
            "type": "email"
        },
        "這是一個普通的字串,不用理他"
    ]
}

這個結構裡有字典 (dict) 也有列表 (list)。我們的遞迴可以這樣寫:

def simple_traverse(node):
    # 只要是字典我們就來檢查
    if isinstance(node, dict):
        # 1. 如果字典裡有 class_name我們就抓到目標了印出來
        if "class_name" in node:
            print(f"找到目標囉!class_name 叫做: {node['class_name']}")
        
        # 2. 為了確保我們沒有漏掉字典更深處的東西我們把字典裡所有的 value 都拿出來再走訪一次
        for key, value in node.items():
            simple_traverse(value)  # 遞迴繼續往下層找
            
    # 如果遇到的是一個列表那就把列表裡的東西一個一個拿出來走訪
    elif isinstance(node, list):
        for item in node:
            simple_traverse(item)   # 遞迴繼續往下層找

# 開始走訪
simple_traverse(data)

輸出結果:

你看,我們完全不需要知道這個 JSON 到底有幾層,
只要透過 simple_traverse(value) 和 simple_traverse(item),
程式就會像藤蔓一樣自動延伸到底部,這就是遞迴的魅力!

第三步:這跟 AST (抽象語法樹) 的關係?
是的,處理 AST 百分之百就是用這種遞迴結構!

當 Python (或其他編譯器) 讀取你的程式碼時,
它會把文字轉換成一棵抽象語法樹 (AST)。
例如你寫了一句 a = 1 + 2,在 AST 裡面可能長這樣 (簡化版):

ast_tree = {
    "type": "Assign",          # 這是一個指派運算 (Assign)
    "targets": ["a"],          # 左邊是要被賦值的變數 "a"
    "value": {                 # 右邊要賦的值是一個加法運算 (BinOp)
        "type": "BinOp",
        "operator": "Add",     # 運算子是加號
        "left": { "type": "Constant", "value": 1 },  # 左邊的常數 1
        "right": { "type": "Constant", "value": 2 }  # 右邊的常數 2
    }
}

這不就跟前面的 JSON 長得一模一樣嗎!

如果開發者想用 AST 工具來檢查這份程式碼「有沒有用到加法」,他們寫的程式碼邏輯,跟你寫在 get_path_for_class_v4.py 裡的 traverse 幾乎是同一個模子刻出來的:

def check_addition_ast(node):
    # 如果這是一個字典型態的節點
    if isinstance(node, dict):
        
        # 如果這個節點是一個 加法運算 (Add)
        if node.get("operator") == "Add":
            print("警告:這段程式碼使用了加法!")
            
        # 繼續遞迴走訪底下的所有節點 (把左邊右邊都拆出來檢查)
        for key, child_node in node.items():
            check_addition_ast(child_node)

check_addition_ast(ast_tree)
# 輸出: 警告這段程式碼使用了加法

總結
遞迴 (Recursion) 最適合用來處理「深度未知的主體」
例如 JSON、資料夾、XML、或是 AST。
get_path_for_class_v4.py 的寫法:
檢查是否為 dict/list -> 做該層該做的事 -> 取出子結點繼續 traverse,
這個骨架其實是所有語法解析器、
甚至爬蟲都在使用的標準設計模式 (Design Pattern)。
等你以後要處理像爬梳 HTML 標籤或是 AST 時,
這個老朋友一定會再跟你見面的!

推薦hahow線上學習python: https://igrape.net/30afN

儲蓄保險王

儲蓄險是板主最喜愛的儲蓄工具,最喜愛的投資理財工具則是ETF,最喜愛的省錢工具則是信用卡