2026年8月31日 星期一

Python一下:用遞迴整理水井村文化資產與導覽路線

《Python一下:從風土資料到智慧生活》第 10 篇

用遞迴整理水井村文化資產與導覽路線

當地方資料像樹枝一樣層層展開,讓函式走進每一個分支,再帶著答案回來。

CH6 函式與遞迴樹狀資料USR 地方實作
學習目標
完成本篇後,你能辨認「基底條件」與「遞迴步驟」,讀懂呼叫堆疊,並以遞迴列印、統計及搜尋巢狀資料;也能判斷何時應改用迴圈。

一、為什麼地方資料會需要遞迴?

一個導覽主題下面可能有多個分類,每個分類又包含景物、故事或產業節點。層數不固定時,預先寫三層、四層迴圈既笨重又容易漏掉。遞迴的想法是:把大問題拆成結構相同的小問題,交給同一個函式處理

資料倫理提醒:本文的分類是程式教學用示意資料,不代表正式文化資產認定。實際公開前,應由社區居民、文史工作者與權責單位共同確認名稱、脈絡與可公開範圍。

二、遞迴的兩個必要零件

零件作用缺少時的結果
基底條件告訴函式何時停止無限呼叫,最後出現 RecursionError
遞迴步驟把問題縮小後再次呼叫自己問題沒有被逐步解決

先用倒數暖身。每次呼叫都把 number 減少 1,直到 0 為止。

def countdown(number):
    if number == 0:          # 基底條件
        print("巡查開始!")
        return

    print(number)
    countdown(number - 1)    # 問題縮小


countdown(3)

呼叫順序是 countdown(3) → countdown(2) → countdown(1) → countdown(0)。函式抵達基底條件後,再逐層返回原本暫停的位置;這些等待中的呼叫保存在「呼叫堆疊」。

三、建立水井村樹狀教學資料

每個節點都是字典,具有「名稱」與「子項目」。葉節點的子項目是空串列,因此同一個函式可以處理所有層級。

cultural_tree = {
    "名稱": "水井村",
    "子項目": [
        {
            "名稱": "地方故事",
            "子項目": [
                {"名稱": "白馬", "子項目": []},
                {"名稱": "烏龜", "子項目": []},
                {"名稱": "姻緣花", "子項目": []},
            ],
        },
        {
            "名稱": "生活與產業",
            "子項目": [
                {"名稱": "柴井車", "子項目": []},
                {"名稱": "智慧養殖", "子項目": []},
                {"名稱": "魚菜共生", "子項目": []},
            ],
        },
    ],
}

四、任務一:展開巢狀導覽清單

1
處理目前節點:依深度印出縮排與名稱。
2
處理所有子節點:把深度加 1,再交給同一函式。
def print_tree(node, depth=0):
    indent = "  " * depth
    print(f"{indent}• {node['名稱']}")

    for child in node["子項目"]:
        print_tree(child, depth + 1)


print_tree(cultural_tree)

子項目 是空串列時,for 不會執行,函式自然返回。這裡的空串列就是隱含的基底條件。

五、任務二:統計全部節點與導覽點

目前節點先算 1,再加上所有子樹的節點數。若只想統計沒有子項目的導覽點,遇到葉節點時回傳 1。

def count_nodes(node):
    total = 1
    for child in node["子項目"]:
        total += count_nodes(child)
    return total


def count_leaves(node):
    if not node["子項目"]:
        return 1
    return sum(count_leaves(child) for child in node["子項目"])


print("全部節點:", count_nodes(cultural_tree))
print("導覽點:", count_leaves(cultural_tree))

六、任務三:搜尋名稱並回傳完整路徑

搜尋不只要知道「找到了」,還要回傳從村落根節點走到目標的路徑。注意參數預設值使用 None,避免不同呼叫共用同一個串列。

def find_path(node, target, path=None):
    if path is None:
        path = []

    current_path = path + [node["名稱"]]
    if node["名稱"] == target:
        return current_path

    for child in node["子項目"]:
        result = find_path(child, target, current_path)
        if result is not None:
            return result

    return None


path = find_path(cultural_tree, "魚菜共生")
if path:
    print(" → ".join(path))
else:
    print("找不到指定導覽點")

七、把葉節點整理成導覽選單

def collect_routes(node, path=None):
    if path is None:
        path = []

    current_path = path + [node["名稱"]]
    if not node["子項目"]:
        return [" → ".join(current_path)]

    routes = []
    for child in node["子項目"]:
        routes.extend(collect_routes(child, current_path))
    return routes


for number, route in enumerate(collect_routes(cultural_tree), start=1):
    print(f"{number}. {route}")

八、遞迴不一定是唯一答案

情境較適合原因
樹狀資料、資料夾、巢狀選單遞迴程式結構貼近資料結構
固定範圍的重複工作for/while直觀且沒有遞迴深度問題
層數可能非常深自行維護 stack 的迴圈避免超過 Python 遞迴深度

以下以串列當作堆疊,完成相同的深度優先走訪:

def print_tree_iterative(root):
    stack = [(root, 0)]

    while stack:
        node, depth = stack.pop()
        print(f"{'  ' * depth}• {node['名稱']}")

        for child in reversed(node["子項目"]):
            stack.append((child, depth + 1))


print_tree_iterative(cultural_tree)

九、真實資料要防範循環參照

純樹狀資料每個節點只會向下;若節點互相連結,就可能繞回已拜訪的位置。可以記錄物件識別值,並設定最大深度。

def safe_print(node, depth=0, max_depth=20, visited=None):
    if visited is None:
        visited = set()
    if depth > max_depth:
        print("  " * depth + "…已達深度上限")
        return

    node_id = id(node)
    if node_id in visited:
        print("  " * depth + "…偵測到重複參照")
        return

    visited.add(node_id)
    print("  " * depth + "• " + node["名稱"])
    for child in node["子項目"]:
        safe_print(child, depth + 1, max_depth, visited)


safe_print(cultural_tree)

十、常見錯誤除錯表

現象可能原因修正方式
RecursionError沒有停止條件,或資料形成循環加入基底條件、visited 與深度上限
一直處理同一筆資料遞迴步驟沒有縮小問題確認傳入的是子節點或較小參數
第二次搜尋出現舊路徑預設參數寫成 path=[]改用 None,函式內再建立串列
明明找到卻得到 None遞迴結果沒有向上回傳接住 result,找到時立刻 return
資料很深時失敗超過 Python 遞迴深度改用自行維護 stack 的迴圈

十一、USR 實作挑戰

挑戰 A|社區共編資料樹
訪談後新增一個分類與三個子節點;為每筆資料加入「來源」與「是否可公開」欄位,列印時只顯示可公開項目。
挑戰 B|規劃主題導覽
加入「建議停留分鐘」欄位,以遞迴計算每個分類及全程所需時間,再找出時間最長的導覽分支。
挑戰 C|改寫成檔案資料
把文化樹存成 JSON,讀取後執行搜尋。這會銜接後續的檔案處理與網頁資料章節。

十二、與生成式 AI 協作

可以請 AI 協助檢查程式,但地方知識仍需由社區確認。以下提示詞可直接修改:

請擔任 Python 助教。以下是一份水井村教學用樹狀資料與遞迴函式。
請依序檢查:
1. 是否有明確的基底條件;
2. 每次遞迴是否讓問題縮小;
3. 回傳值是否能逐層傳回;
4. 是否有循環參照或過深風險。
請只提出程式結構建議,不要自行補寫未經社區確認的地方史實。
本篇小結
遞迴不是「函式一直叫自己」而已,而是:先定義停止點,再讓每一步都更接近停止點。面對地方文化分類、導覽目錄與其他巢狀資料時,它能讓程式像資料本身一樣清楚。

沒有留言:

張貼留言