🗺️ 程式互動式地圖
零基礎起步・第 13 課

遞迴與呼叫堆疊

函式裡面呼叫自己,為什麼不會一直重複下去?它到底什麼時候停、答案又是怎麼算出來的?這一課一步一步看呼叫框一層層疊上去,再帶著回傳值一層層拿掉。

呼叫框疊上去、再拿掉 終止條件 遞迴與迴圈互換

💡 先搞懂問題

第一次看到下面這種程式,多數人會愣一下:函式 total 的本體裡,竟然又寫著 total(n - 1)。自己呼叫自己,聽起來像兩面對照的鏡子,影像一層套一層沒有盡頭;可是這段程式一執行,馬上印出 10,乾淨俐落。它到底什麼時候停下來?答案 10 又是在哪一行、怎麼算出來的?

def total(n):              # 算 1 加到 n 的總和
    if n == 1:
        return 1
    rest = total(n - 1)     # 函式裡呼叫自己
    return n + rest
print(total(4))             # 印出 10

這種「函式在本體裡呼叫自己」的寫法叫遞迴(recursion)。它背後的想法其實很日常:1 加到 4 有點麻煩,但如果有人先告訴你「1 加到 3 是 6」,你只要再加上自己的 4 就好;而「1 加到 3」又可以拆成「1 加到 2,再加 3」……一直拆到「1 加到 1」,這個小到不用算,答案就是 1。遞迴就是把大問題拆成「自己的一小份」加上「同樣形狀、但小一號的問題」,小一號的問題再交給同一個函式去處理。

要看懂遞迴,關鍵是第 9 課學過的呼叫框(frame):每呼叫一次函式就開一個新框,放這次呼叫自己的參數和區域變數;函式呼叫函式時,框會一層層疊起來,叫呼叫堆疊(call stack)。遞迴只是「被呼叫的函式剛好是自己」,Python 照樣替每一次呼叫開一個新框,所以 total(4)、total(3)、total(2)、total(1) 會各自有一個 n,互不干擾。

生活比喻:簽書會隊伍裡問「你排第幾個?」

虛構的「北辰書店」辦簽書會,隊伍沿著騎樓排得很長。阿哲排在中間,看不到隊伍最前面,想知道自己排第幾個。他不必跑到前面一個一個數,只要拍拍前面那個人問:「你排第幾個?」前面那位也不知道,就照同樣的方法問他前面的人,然後站著等回答。這樣一路往前問,問到排在最前面的那位讀者,她前面已經沒有人了,不用再問任何人,直接回答:「我是第 1 個。」

接著答案開始往回傳:第 2 位聽到「1」,加 1,回頭告訴後面的人「我第 2 個」;第 3 位聽到「2」,加 1……最後阿哲聽到前面的人說「我第 4 個」,自己就是第 5 個。每個人都只做了兩件小事:往前問一次、聽到答案後加 1 再往後說,整條隊伍卻算出了正確的位置。

① 往前問:每問一次,就離排頭近一個人 簽書 桌 排頭???阿哲 你排第幾個?你排第幾個?你排第幾個?你排第幾個? 我第 1 個我第 2 個我第 3 個我第 4 個 ② 答案往回傳:拿到前面的答案加 1,再告訴後面的人 排頭前面沒有人,不用再問就能直接回答;阿哲聽到 4,加 1,知道自己第 5 個
上排紫色虛線是「往前問」,問題一路傳到排頭;排頭不用再問,直接給出答案。下排橘色箭頭是「答案往回傳」,每個人只做一件事:把聽到的數字加 1,再告訴後面的人。
回到程式:隊伍裡每一個人,都是函式的一次呼叫,各自有一個呼叫框;大家照著同一個規則做事,就像每一次呼叫執行的都是同一個函式 total。拍拍前面的人問一次,就是在函式裡呼叫自己,而且問題小一號:total(n - 1)。排頭不用再問、直接回答,這種「問題小到可以直接給答案」的情況叫終止條件(base case),寫成 if n == 1: return 1。問完站著等答案的人,就是暫停在半路、疊在呼叫堆疊裡的呼叫框;答案一個個往後傳,就是回傳值(return value)一層層交回呼叫它的那一層。阿哲那一句「聽到 4,加 1」,對應到程式裡的 return n + rest:拿到小一號問題的答案,加上自己的那一份。
簽書會隊伍(比喻) Python 的對應 排隊的每一個人 一次呼叫,各有自己的呼叫框 大家照同一個規則做 每次執行的都是同一個函式 拍拍前面的人問一次 呼叫自己 total(n - 1) 每問一次,離排頭更近 n 每次少 1,更接近終止 排頭直接回答「第 1 個」 終止條件 base case 問完站著等答案 呼叫框暫停,疊在呼叫堆疊裡 聽到答案加 1,再往後說 回傳值交回上一層 return
左欄是簽書會隊伍,右欄是 Python 的正式說法。前四列是「往前問」的階段,呼叫框越疊越多;後三列是「答案往回傳」的階段,呼叫框一個個拿掉。

這個比喻有三個地方和 Python 不一樣。第一,隊伍裡的人本來就站在那裡;Python 的呼叫框卻是呼叫時才開、回傳後就拿掉,所以「往前問」的階段堆疊越長越高,「往回傳」的階段又一層層縮回去。第二,真實的隊伍一定有排頭;程式裡如果忘了寫終止條件,或是每次呼叫並沒有讓問題變小,Python 就會一直「往前問」下去,直到疊了大約一千層,被迫停下並報錯(原理補完第 4 小節會看到那個錯誤畫面)。第三,隊伍裡是不同的人,程式裡卻是同一個函式被呼叫了好幾次:程式碼只有一份,但每一次呼叫都有自己的框、自己的 n,就像每個人手上都拿著同一張規則卡,卡上寫的「我的位置」各填各的。

全域(主程式) answer = total(4) total(4) n = 4 4 + total(3) → 4 + 6 total(3) n = 3 3 + total(2) → 3 + 3 total(2) n = 2 2 + total(1) → 2 + 1 total(1) n = 1 終止條件:直接 return 1 回傳 1回傳 3回傳 6回傳 10 紫色實線=呼叫:往下疊一層 橘色虛線=回傳:拿掉一層、往上交 往下 4 次、往上 4 次, answer 最後貼到 10
這是實驗室一要逐行執行的程式。每一層都卡在「4 + total(3)」這種式子的一半:右邊的呼叫還沒回來,加法就做不了,只好往下再問一層。直到 total(1) 碰到終止條件,答案才從最底層開始往回傳,每一層補上自己的加法:1 → 3 → 6 → 10。

🎮 互動實驗室一:呼叫框疊上去,再一層層拿掉

這是上面那張樓梯圖的程式,Python 3.13 真的執行一次、逐步記錄下來的結果。按「下一步」,綠色箭頭是下一個要執行的行,黃色是剛執行完的行,灰色 ⏸ 是「呼叫了函式、正在等它回來」的行。右下方的記憶體圖左邊是名字,每一個方框是一個呼叫框;右邊是物件。請盯著左欄數框的數量:往下呼叫時越疊越多,碰到終止條件後,每回傳一次就少一個。停在 🤔 的地方,先猜再按下一步。

畫面說明:第 12 步是堆疊最高的時候:全域加上 4 個 total 的框,每個框裡的 n 各指向 4、3、2、1;程式碼的第 6 行和第 4 行標著灰色 ⏸,代表全域停在第 6 行、上面三層 total 都停在第 4 行等答案。從第 15 步開始看「回傳值」那一列:它出現在最下面的框裡,下一步那個框就被拿掉,回傳值改由上一層的 rest 貼著。第 18 步 n 和 rest 都指向同一個 3,那是 Python 重用了同一個不可變的整數,不是箭頭畫錯。

🎮 互動實驗室二:倒數計時,事情是去程做還是回程做?

遞迴函式裡的程式碼,可以寫在「呼叫自己」的前面,也可以寫在後面。寫在前面的,會在往下疊的時候執行;寫在後面的,要等下一層回來、往回拿掉的時候才執行。右上角的選單有三段倒數計時:第一段先印再呼叫,第二段先呼叫再印(只把兩行對調),第三段用 while 迴圈改寫。先猜輸出畫面的順序,再一步一步驗證。

畫面說明:「先印再呼叫」那段,每疊一層就印一個數字,輸出畫面在往下的路上就依序出現 3、2、1、開門!,回程時四個框只是一一拿掉,什麼都沒印。切到「先呼叫再印」,往下的路上什麼都沒印,直到最底層印出「開門!」,回程才依序印出 1、2、3:數字順序整個顛倒。切到「用 while 迴圈改寫」,左欄從頭到尾只有一個 countdown 的框,n 在同一個框裡改貼 3、2、1、0。

🎮 互動實驗室三:換你當 Python

這次沒有播放器幫你走,由你來決定每一步。最下面那個框是「正在執行」的那一層:先判斷它碰到終止條件了沒有,沒碰到就再疊一個框往下問;碰到了就回傳,接著替上一層算出它該回傳的值。可以選「加總」或「階乘」兩個函式:階乘(factorial)是 1 乘到 n,例如 4 的階乘是 4 × 3 × 2 × 1 = 24,寫法和加總只差一個符號。n 選大一點,看堆疊能疊多高。所有答案都和 Python 3.13 實際執行的結果相同。

函式
n =
目前呼叫框 1 個
最多疊到 1 個
判斷錯誤 0 次
    呼叫堆疊(越下面越晚開)框裡是這一層自己的名字
    畫面說明:左邊是這一關要執行的程式;右邊每個框都是同一個函式的一次呼叫,框裡的 n 各不相同。

    📘 原理補完

    1. 正式名稱一次對照

    看官方文件或別人的教學時,會碰到下面這些名詞。左邊兩欄是正式說法,右邊兩欄對應到這一課的程式和播放器畫面。

    用語英文在程式裡長怎樣白話意思
    遞迴recursion函式本體裡寫著 total(n - 1)函式呼叫自己,把問題交給小一號的自己處理
    遞迴函式recursive functiondef total(n): 這整個函式會呼叫自己的函式
    終止條件base caseif n == 1: return 1問題小到可以直接回答,不再呼叫自己
    遞迴步驟recursive caserest = total(n - 1)、return n + rest把問題縮小一點交出去,拿到答案後補上自己的一份
    呼叫堆疊call stack播放器左欄疊在一起的框每呼叫一次疊一個框,回傳就拿掉最下面(最新)的那個
    遞迴深度recursion depthtotal(4) 疊到 4 個 total 的框同時疊著幾層呼叫
    遞迴深度上限recursion limit預設 1000疊太深時 Python 會停下來報錯,保護自己不當掉
    RecursionError(錯誤類型)maximum recursion depth exceeded疊超過上限了,通常是終止條件沒寫或永遠碰不到

    「堆疊(stack)」這個詞本身就是「一疊東西」的意思,像一疊盤子:新的盤子只能放在最上面,要拿也只能先拿最上面那個,也就是後放進去的先拿出來。呼叫堆疊正是這樣運作:最後開的框一定最先結束。播放器把新的框畫在下面,所以「最上面」在畫面上是最下面那一個,這只是畫法不同。

    2. 寫遞迴的兩個要件

    一個遞迴函式能正常結束,一定同時滿足兩件事:有終止條件,而且每一次呼叫自己,參數都更接近終止條件。total(n) 的終止條件是 n == 1,每次呼叫傳的是 n - 1,所以從 4 出發,經過 3、2,一定會碰到 1。兩個條件少了任何一個,函式就會一直呼叫下去(第 4 小節)。

    total(n) n == 1 ? return 1 ① 終止條件 成立 rest = total(n - 1) ② n 變小,更接近 1 不成立 return n + rest 虛線:同一個函式再從頭跑一次(開新的框) 不再呼叫,開始往回傳
    每一次呼叫都走同一張流程圖,只是 n 不同。① 是出口:只要有一條路不再呼叫自己,呼叫才有機會停止。② 保證會走到出口:每繞一圈,n 都比上一圈小 1,總有一圈會等於 1。

    自己寫遞迴時,可以照下面三個問題想,順序很重要:

    1. 最小的情況是什麼?答案直接是多少?例如「1 加到 1」就是 1、「空串列的總和」是 0、「空字串反過來」還是空字串。這就是終止條件。
    2. 大問題怎麼用「小一號問題的答案」組出來?先假設小一號的答案已經有人算好了(就像阿哲相信前面的人會回答),只想「拿到之後要怎麼補上自己的一份」,例如 n + rest、n * rest。
    3. 每次呼叫,參數有沒有往終止條件靠近?n 減 1、串列少一個元素、資料夾往下一層,都算靠近;參數原封不動傳下去,或是會跳過終止條件,就會停不下來。

    第二點是遞迴最需要練習的地方:不要試著在腦中把每一層都展開,只要相信「小一號的那一層會給我正確答案」,專心寫好這一層該做的事。展開每一層的工作,交給播放器或除錯器就好。

    3. 呼叫堆疊有多高?

    每一層呼叫在等下一層回來時,它的框都還留在記憶體裡,所以堆疊的高度就是「同時有幾層還沒結束」。total(4) 碰到終止條件的那一刻,記憶體裡同時有 5 個框:全域加上 4 個 total。total(n) 會疊到 n 層,n 越大疊得越高;這也是遞迴比迴圈多花記憶體的地方。

    堆疊最高的時候(播放器第 12~14 步) 全域(主程式)total total() 的呼叫框n total() 的呼叫框n total() 的呼叫框n total() 的呼叫框n 函式total(n) 整數 int4 整數 int3 整數 int2 整數 int1 ⏸ 停在第 6 行,等 total(4) ⏸ total(4):停在第 4 行,等 total(3) ⏸ total(3):停在第 4 行,等 total(2) ⏸ total(2):停在第 4 行,等 total(1) ➜ total(1):n == 1,準備 return 1 越下面越晚開 共 5 個框:全域 + 4 個 total;只有最下面那個在執行,其餘都在等
    畫法和播放器相同:左邊名字、右邊物件。四個框的名字都叫 n,卻各自貼在不同的整數上,因為它們分屬四次不同的呼叫。rest 還沒出現在任何一框裡:每一層都卡在 rest = total(n - 1) 的等號右邊,要等下一層回來才貼得上。

    回傳時則反過來:最下面的框 return 之後立刻被拿掉,回傳值交給它上面那一框,那一框從暫停的第 4 行繼續,把值貼到 rest、算出自己的答案、return、被拿掉……一路往上,直到全域的 answer 拿到 10。回傳值一次只往上傳一層,不會直接從最底層跳回全域;每一層都要親手補上自己的那一份加法。

    4. 沒有終止條件:RecursionError

    如果把實驗室二「先印再呼叫」的倒數計時拿掉 if n == 0 那一段,函式就只剩「印出 n,再呼叫自己」,沒有任何一條路會停下來。下面是 Python 3.13 實際執行的畫面:數字從 3 一路印到 -995,接著出現錯誤。

    def countdown(n):
        print(n)
        countdown(n - 1)
    countdown(3)
    Python 3.13 的輸出畫面(共 999 個數字,中間省略)
    3
    2
    1
    0
    -1
    ⋮(中間省略 989 行)
    -991
    -992
    -993
    -994
    -995
    Traceback (most recent call last):
      File "/tmp/b0_recur/main.py", line 4, in <module>
        countdown(3)
        ~~~~~~~~~^^^
      File "/tmp/b0_recur/main.py", line 3, in countdown
        countdown(n - 1)
        ~~~~~~~~~^^^^^^^
      File "/tmp/b0_recur/main.py", line 3, in countdown
        countdown(n - 1)
        ~~~~~~~~~^^^^^^^
      File "/tmp/b0_recur/main.py", line 3, in countdown
        countdown(n - 1)
        ~~~~~~~~~^^^^^^^
      [Previous line repeated 996 more times]
    RecursionError: maximum recursion depth exceeded

    照第 10 課的方法從最後一行讀起:RecursionError: maximum recursion depth exceeded 的意思是「超過遞迴深度的上限」。Python 預設最多讓呼叫堆疊疊到 1000 層,超過就拋出這個錯誤,免得框無止境地疊下去、把記憶體吃光,讓整個程式甚至 Python 本身當掉。往上看 Traceback:同樣的 line 3, in countdown 只印了三次,接著一行 [Previous line repeated 996 more times],意思是「上一行又重複了 996 次」,Python 幫你把一模一樣的上千層摺疊起來。數一數:全域 1 層,加上 3 + 996 = 999 層 countdown,剛好 1000 層,這也是為什麼畫面上正好印了 999 個數字。

    全域(第 1 層) countdown n = 3 countdown n = 2 countdown n = 1 countdown n = 0 countdown n = -1 ⋮ countdown n = -995 ← 第 1000 層:碰到上限 n == 0 時沒有任何程式叫它停, 於是繼續呼叫 countdown(-1)… 每一層都還沒結束,框一個都拿不掉, 堆疊只會越疊越高。 Python 的保護:預設最多 1000 層, 超過就拋出 RecursionError。 (圖中只畫出開頭與結尾幾層;層數依 Python 3.13 實際執行的結果)
    和第 8 課的無窮迴圈不同,無窮遞迴不會一直跑到你按下停止:每一層都佔著一個框,Python 數到上限就會自己喊停。不過看到 RecursionError,該做的不是把上限調高,而是回頭檢查兩個要件。

    上限可以用內建模組 sys 的 sys.getrecursionlimit() 查詢、用 sys.setrecursionlimit() 調整(要先 import sys,見 模組與 import 節點)。官方文件特別提醒:調得太高可能讓 Python 直接當掉。初學階段碰到這個錯誤,幾乎都是程式本身的問題,而不是上限太低。

    有終止條件,卻永遠碰不到。第二個要件被打破時,畫面一模一樣。下面這段想算「5 + 3 + 1」,每次減 2,終止條件卻寫成 n == 0:n 從 5 變成 3、1、-1、-3……正好跳過 0,永遠不會成立。

    def total(n):
        if n == 0:
            return 0
        return n + total(n - 2)
    print(total(5))
    Traceback (most recent call last):
      File "/tmp/b0_recur/main.py", line 5, in <module>
        print(total(5))
              ~~~~~^^^
      File "/tmp/b0_recur/main.py", line 4, in total
        return n + total(n - 2)
                   ~~~~~^^^^^^^
      File "/tmp/b0_recur/main.py", line 4, in total
        return n + total(n - 2)
                   ~~~~~^^^^^^^
      File "/tmp/b0_recur/main.py", line 4, in total
        return n + total(n - 2)
                   ~~~~~^^^^^^^
      [Previous line repeated 996 more times]
    RecursionError: maximum recursion depth exceeded

    改法是讓終止條件涵蓋所有可能停下的情況,例如寫成 if n <= 0:。同樣的錯也會發生在「忘了讓參數變小」:把 total(n - 1) 誤寫成 total(n),每一層拿到的都是同一個 n,錯誤畫面也和上面一樣。

    忘了寫 return。這是另一個常見的錯,而且錯誤訊息不是 RecursionError。下面第 4 行算出了 n + total(n - 1),卻沒有把它 return 出去,函式走到結尾,回傳的是 None(第 9 課):

    def total(n):
        if n == 1:
            return 1
        n + total(n - 1)
    print(total(3))
    Traceback (most recent call last):
      File "/tmp/b0_recur/main.py", line 5, in <module>
        print(total(3))
              ~~~~~^^^
      File "/tmp/b0_recur/main.py", line 4, in total
        n + total(n - 1)
        ~~^~~~~~~~~~~~~~
    TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'

    仔細看會發現一件有趣的事:Traceback 只有兩層。total(1) 正常回傳 1;total(2) 算出 2 + 1 = 3,但沒有 return,所以交回去的是 None;輪到 total(3) 算 3 + None 時才出錯。錯誤出現在上層,真正漏寫的那一行卻是每一層都有的第 4 行。遞迴函式裡「會回傳結果的路線」,每一條都要寫 return。

    5. 遞迴與迴圈可以互相改寫

    實驗室二的第三段已經看到:倒數計時用 while 迴圈也寫得出來。事實上,任何遞迴都能改寫成迴圈,反過來也一樣,差別在於「重複的狀態放在哪裡」。迴圈從頭到尾只有一個框,靠同一個名字一直改貼來記住進度;遞迴則是每一層開一個新框,進度記在疊起來的那一疊框裡。下面用階乘對照兩種寫法:

    迴圈:fact_loop(4) 遞迴:fact(4) fact_loop() 的呼叫框(只有這一個) n4 result1 → 2 → 6 → 24 k2 → 3 → 4 fact n = 4 fact n = 3 fact n = 2 fact n = 1 12624 箭頭「→」是同一個名字依序改貼 框只有 1 個,進度記在 result 框疊到 4 個,進度記在這一疊框裡 右邊的橘字=各層回傳值,回程時由下往上算出
    同樣算出 24。左邊的迴圈只用一個框,result 每一圈改貼一次;右邊的遞迴每一層各開一個框,答案在回程才從 1 → 2 → 6 → 24 一層層算出來(示意,畫法簡化)。
    迴圈(for、while)遞迴
    怎麼重複同一段程式回到迴圈開頭再跑一次函式呼叫自己,同一段程式在新的框裡再跑一次
    進度記在哪同一個框裡的變數,一直改貼每一層框自己的參數與區域變數
    怎麼停for 拿完元素、while 條件變成 False碰到終止條件,不再呼叫自己
    寫錯停不下來時無窮迴圈,一直跑到你手動中止疊到上限(預設 1000 層),拋出 RecursionError
    記憶體框只有一個,不會越用越多深度越深,同時留著的框越多
    最適合一排資料逐一處理、次數固定的重複樹狀、一層包一層的資料(資料夾、巢狀串列、JSON)
    # 同一件事的兩種寫法:5 的階乘 5 × 4 × 3 × 2 × 1
    def fact(n):
        if n == 1:                  # 終止條件:1 的階乘就是 1
            return 1
        return n * fact(n - 1)      # 自己的 n 乘上「小一號問題」的答案
    
    def fact_loop(n):
        result = 1
        for k in range(2, n + 1):   # k 依序是 2、3、…、n
            result = result * k     # 同一個呼叫框裡,result 一直改貼
        return result
    
    print(fact(5), fact_loop(5))
    print(fact(1), fact_loop(1))
    120 120
    1 1

    像加總、階乘、倒數這種「一條直線往下」的問題,用迴圈其實更直接,也不必擔心疊太深;它們在這一課出現,是因為形狀簡單、最適合用來看清楚呼叫框怎麼疊。Python 本身也不鼓勵用遞迴處理很長的資料,例如把一萬個元素的串列用遞迴一個一個加,一定會撞到 1000 層的上限,這種情況請用迴圈或內建的 sum()。

    6. 什麼時候用遞迴比較自然:一層包一層的資料

    遞迴真正好用的地方,是資料本身就是「大的裡面有同樣形狀的小的」:資料夾裡有檔案,也有資料夾,裡面的資料夾又可能有資料夾;串列裡有數字,也有串列(第 11 課的巢狀資料)。這種資料,你事先不知道會包幾層,用固定幾層的 for 迴圈很難寫,用遞迴卻很自然:碰到數字(或檔案)就直接處理,碰到串列(或資料夾)就交給同一個函式,裡面還有幾層,由它自己去處理。

    [1, [2, 3], [4, [5]]] 1 [2, 3] [4, [5]] 2 3 4 [5] 5 回傳 15回傳 5回傳 9回傳 5 串列:呼叫一次 nested_sum 數字:直接加進 total 樹有幾層,呼叫框最多就疊幾層(這裡 3 層)
    把巢狀串列畫成一棵倒過來的樹:紫色的串列都是「同樣形狀、但小一號」的問題,每個都交給 nested_sum 處理一次;藍色的數字是最小的情況,直接加。橘色是每個串列交回去的總和,從下往上合成 15。

    下面的播放器就是這棵樹的程式。type(x) == list 用第 3 課的 type() 判斷 x 是不是串列(實務上更常寫成 isinstance(x, list),意思相同)。這個函式沒有寫成 if …: return 的終止條件,但它其實有:串列裡都是數字時,for 迴圈跑完就 return,不會再呼叫自己;而每次呼叫傳進去的都是更內層、更小的串列,一定會走到底。

    畫面說明:右欄的大串列有兩格不是數字,而是箭頭,指向另外兩個串列方塊,這就是第 11 課的巢狀資料。第 12 步呼叫 nested_sum(x) 時,新框的 items 和上一層的 x 指向同一個 [2, 3]:參數貼的是同一個物件,沒有複製。最深的時候(第 34 步起)左欄有 4 個框。各框的 total 互不相干:外層的 total 停在 1 或 6 等待,內層從 0 開始重新累加。有時 total 和 x 指向同一個整數(例如都是 5),那是 Python 重用了相同的不可變整數。

    同樣的寫法換成資料夾:用字典表示一個資料夾,鍵是檔名,值是檔案大小(KB)或另一個字典(子資料夾)。函式一邊印出樹狀目錄,一邊算出總大小;depth 參數記錄現在在第幾層,每往下一層加 1,用來決定縮排幾格。

    # 北辰書店的共用資料夾:資料夾裡還有資料夾(用字典表示,數字是檔案大小 KB)
    shared = {
        "報價單.pdf": 120,
        "門市照片": {
            "外觀.jpg": 300,
            "活動": {"簽書會.jpg": 250, "海報.png": 180},
        },
        "說明.txt": 4,
    }
    
    def folder_size(folder, depth):
        size = 0
        for name, item in folder.items():
            print("  " * depth + name)                    # 依深度縮排,印出樹狀目錄
            if type(item) == dict:                         # 是資料夾:交給同一個函式處理
                size = size + folder_size(item, depth + 1)
            else:                                          # 是檔案:直接加上大小
                size = size + item
        return size
    
    print("合計", folder_size(shared, 0), "KB")
    報價單.pdf
    門市照片
      外觀.jpg
      活動
        簽書會.jpg
        海報.png
    說明.txt
    合計 854 KB

    注意輸出的順序:「門市照片」底下的兩層全部印完,才輪到最外層的「說明.txt」。這是因為呼叫 folder_size 處理「門市照片」時,最外層的 for 迴圈停在半路等它回來,就像實驗室一裡停在第 4 行的那幾個框。如果想讓這個資料夾再多包十層,函式一個字都不用改。

    7. 以後會在哪裡遇到

    自己寫遞迴的機會也許不多,但「一層包一層」的資料在 AI 與資料分析裡到處都是:API 回傳的 JSON 是字典裡有串列、串列裡又有字典,要把它攤平成表格時常會寫一個遞迴函式;決策樹(decision tree)模型本身就是一棵樹,畫出它的規則或解釋一筆預測時,就是從樹根一層層往下走;處理資料夾裡的所有檔案、把整份巢狀資料完整複製一份(第 11 課提過,淺複製只複製外層)也是同樣的形狀。讀到這類程式時,先找終止條件在哪、再看每次呼叫傳進去的東西是不是變小了,就能抓到它怎麼運作。另外,第 10 課讀 Traceback 時那一疊 File,就是出錯當下的呼叫堆疊;遞迴出錯時你會看到同一個函式名稱重複好幾層,現在你知道為什麼了。

    ✅ 自我檢測

    6 題原創的程式閱讀題,每題讀一小段遞迴程式、選出結果。所有答案都用 Python 3.13 實際執行確認過;輸出有好幾行時,選項裡用 ⏎ 表示換行。選完會立即顯示對錯與解析,全部作答後出現總分。目前得分:0 / 6