ECCH4.1 模組性與拆解:為何要寫子程式?
重點
- 子程式把「大問題」拆成「可管理的小步驟」,提升模組性與可讀性。
- 把重複工作封裝成子程式,可避免多處重覆修改,降低出錯機會。
- 拆解時要思考子程式的介面:需要哪些參數?應回傳甚麼?
定義
- 模組性(modularity):把程式分成多個「功能明確、彼此相對獨立」的部分,方便理解、測試與維護。
- 拆解:把大問題分成多個小問題;每個小問題都可以由一個子程式處理。
- 子程式(sub-program):可被調用的程式片段;通常會接收參數、執行工作,並(可選)回傳結果。
原理/運作
- 把子程式想像成「黑盒」:外界只需要知道輸入(參數)與輸出(回傳值),不用關心內部細節。
- 理想情況下,一個子程式只做一件清楚的事(例如:計算平均、檢查是否合格、格式化輸出)。
- 主流程(main flow)負責串連步驟;子程式負責處理細節,令程式結構更清晰。
例子
要輸入三科分數並輸出平均分,你可以先拆成兩個子程式:
sum3(m1, m2, m3):回傳總分avg3(total):回傳平均
def sum3(m1, m2, m3):
return m1 + m2 + m3
def avg3(total):
return total / 3
t = sum3(72, 64, 80)
a = avg3(t)
print("平均分:", a)
比較
| 做法 | 優點 | 限制/風險 |
|---|---|---|
| 所有步驟寫在同一段 | 短程式可能較快寫完 | 程式一長就難讀、難測、難改;重複片段更易出錯 |
| 拆成多個子程式 | 結構清晰、可重用、可獨立測試 | 需要先想清楚介面(參數/回傳值)與命名 |
常見錯誤
- 拆得太碎:每幾行就一個子程式,反而令程式跳來跳去,難以追蹤。
- 拆得不清:子程式名字含糊(例如
do()、process()),或同一子程式做太多事。 - 介面設計混亂:參數太多、回傳值不清楚,最後變成「調用者也要知道內部細節」。
技巧:先用一句話描述子程式「做甚麼」,再寫函數名。若一句話說不清,通常表示它做太多事。
寫子程式不是為了「把程式切開」而已,而是為了令程式的結構更清晰:主流程負責描述整體步驟, 子程式負責完成某個小任務。當你能把任務拆到「每一段都可以用一句話說明」時,整體程式通常就容易理解。
拆解亦能提升維護能力:如果同一段計算在多處出現,將它封裝成子程式後,將來改規則只需改一處。 對於較大型的程式,這種「改動集中、影響一致」的特性非常重要。
圖像化理解:結構圖(Structure Chart)
觀察重點:主流程只描述「做事順序」,每個子程式只負責一項任務。這樣寫出來的程式較容易閱讀與測試。
拆解思考表
| 你可以問自己 | 目的 | 例子 |
|---|---|---|
| 這段程式在其他地方會再用嗎? | 找出可重用模組 | 計算折扣、檢查合格、格式化輸出 |
| 這段程式能否用一句話說明? | 確保子程式責任清晰 | 「計算平均分」比「處理資料」更清楚 |
| 外界需要給它甚麼資料? | 決定參數 | 半徑 r、長與闊 w/h、分數列表 |
| 外界需要得到甚麼結果? | 決定回傳值 | 回傳面積、回傳 True/False、回傳總分 |
實作練習(Python)
練習 1(對應重點 1):把重複輸出變成子程式 sayfavsubject()
原本你可能會寫:
print("I love ICT")
.
.
.
.
print("I love ICT")
可以改成先定義子程式,再調用多次:
def sayfavsubject():
print("I love ICT")
下面程式已經調用了 5 次 sayfavsubject()。你只需要把第一行改成 def sayfavsubject():。
顯示參考答案
def sayfavsubject():
print("I love ICT")
sayfavsubject()
sayfavsubject()
sayfavsubject()
sayfavsubject()
sayfavsubject()
練習 2(對應重點 2):把重複輸出封裝成 banner()
如果你要多次輸出同一條分隔線,原本可能會重複寫很多次 print("=" * 10)。
你可以把「分隔線」封裝成無參數子程式 banner(),再按次序調用。
顯示參考答案
def banner():
print("=" * 10)
text = input()
banner()
print(text)
banner()
banner()
print("完")
banner()
練習 3(對應重點 3):設計介面 calc_discount(price, rate)
讀入 price(原價)與 rate(折扣率,例如 0.25)。
請設計子程式介面 calc_discount(price, rate):子程式只負責計算並回傳折後價(不要在子程式內做 input() 或 print()),
再由主流程輸出 final=...。
顯示參考答案
def calc_discount(price, rate):
return price * (1 - rate)
price = float(input())
rate = float(input())
final = calc_discount(price, rate)
print("final=" + str(final))
def avg(m1, m2, m3):
total = m1 + m2 + m3
return total / 3
print("平均分:", avg(72, 64, 80))
子程式 avg(m1, m2, m3)
total ← m1 + m2 + m3
回傳 total / 3
結束子程式
輸出 "平均分:", avg(72, 64, 80)
Check Point(四選一)
ECCH4.2 子程式的定義與調用:由「寫一次」到「用多次」
重點
- 先定義子程式,再調用子程式:順序與縮排必須正確。
- 子程式名稱要能反映功能;介面(參數/回傳值)要清楚,調用者才容易使用。
- 一個子程式應集中處理一項任務,避免把輸入、計算、輸出全部混在一起。
定義
- 定義(define):在程式中寫出子程式內容,告訴電腦「這個名字代表甚麼動作」。
- 調用(call):在需要使用功能的位置,使用子程式名稱(及括號)執行該子程式。
- 回傳:子程式把計算結果交回調用位置,讓主流程繼續使用。
原理/運作
- Python 讀到
def時,只是「記住」子程式內容;真正執行要等到調用。 - 調用時,程式流程會跳到子程式內執行;完成後回到調用位置的下一行。
- 若子程式有回傳值,調用表達式會被替換成回傳值(例如
ans = square(7))。
例子
def hello():
print("Hello, ICT!")
hello()
hello()
同一段輸出只需寫一次,將來要改訊息,也只需改 hello() 的內容。
比較
- 直接寫:快,但容易重複;程式一長就難維護。
- 封裝:初期要多想名字與介面,但可重用、可測試、可讀性高。
常見錯誤
- 忘記括號:
hello與hello()的意思不同;後者才是調用。 - 縮排錯誤:子程式內容必須在
def後縮排。 - 命名不清:
do()、work()等名稱難以反映用途。
在設計子程式時,先用一句話寫下「它要做甚麼」,再把這句話濃縮成恰當的名稱。
例如「把攝氏轉華氏」就很適合叫 fahrenheit(c);名稱越清楚,主流程就越像在閱讀一份步驟說明。
另外,子程式最好做到「輸入清楚、輸出清楚」:需要的資料由參數傳入,計算結果以回傳值交回。 若把輸入與輸出全部寫進子程式內,會令子程式難以重用,亦不便測試。
圖像化理解:調用 → 執行 → 返回
調用就像「去做一件事」:做完後會回到原來位置的下一行;若有回傳值,主流程可以把它當作普通數值使用。
概念對照表:定義與調用
| 概念 | 你在程式中看到的樣子 | 作用 |
|---|---|---|
| 定義 | def f(...): | 建立子程式(不一定立刻執行) |
| 調用 | f(...) | 真正執行子程式 |
| 回傳值 | return ... | 把結果交回調用位置 |
實作練習(Python)
練習 1(對應重點 1):定義 hello() 並調用
主程式已經寫好 hello()。你只需要補上 def hello(): 這一行(注意縮排)。
顯示參考答案
def hello():
print("Hello, ICT!")
hello()
練習 2(對應重點 2):按特定次序調用兩個子程式
已定義兩個子程式 draw_border() 和 draw_message()(都沒有參數)。請只透過調用它們,輸出以下三行:
---------- I love ICT ----------
提示:不要修改兩個子程式內部,只需要在主程式加入調用語句,次序要正確。
顯示參考答案
def draw_border():
print("----------")
def draw_message():
print("I love ICT")
draw_border()
draw_message()
draw_border()
def greet():
print("歡迎使用子程式!")
greet()
greet()
子程式 greet()
輸出 "歡迎使用子程式!"
結束子程式
greet()
greet()
Check Point(四選一)
ECCH4.3 參數傳遞與介面設計:形式參數、實際變元
重點
- 參數(parameter)讓子程式接收外界資料;調用時提供變元(argument)作輸入。
- 形式參數(formal parameter)是子程式定義時寫的名字;實際變元(actual argument)是調用時真正傳入的數值或變量。
- 介面設計要平衡:參數太多會難用;可用資料結構(例如陣列/字串)減少參數數量。
定義
- 參數(parameter):子程式接收輸入資料的「入口」;在定義中出現。
- 變元(argument):調用子程式時,實際傳入的資料(可以是常數、變量、算式)。
- 形式參數(formal parameter):定義子程式時的參數名稱(例如
def f(x):的x)。 - 實際變元(actual argument):調用子程式時提供的值/變量(例如
f(10)的10)。
原理/運作
- 最常見情況:位置對應——第一個實際變元對應第一個形式參數,如此類推。
- 調用時,Python 會把實際變元的值交給形式參數,子程式內就可以用形式參數名字使用這些資料。
- 完成後若有回傳值,調用表達式會得到結果。
例子
def area_circle(r):
pi = 3.14159
return pi * r * r
radius = 3
print(area_circle(radius))
r 是形式參數;radius(其值為 3)是實際變元。
子程式內部不用知道外面變量叫甚麼,只要使用 r。
比較
- 參數太少:子程式需要依賴全程變量或內部輸入,重用性下降。
- 參數太多:調用時容易傳錯次序,閱讀困難。
- 改良方法:把相關資料組合成資料結構(例如陣列、字典),用一個參數傳入。
常見錯誤
- 把參數次序傳錯:
diff(a, b)與diff(b, a)可能得到完全不同結果。 - 以為形式參數名稱要與外面變量同名:其實不用;對應關係主要由「位置」決定。
- 子程式內改動參數就等於改動外面變量:對於不可變資料(例如整數、字串)不一定成立;要分清資料類型與改動方式。
延伸
- 按值調用(call by value):子程式收到值的副本,內部改動唔影響外面。
- 按址調用(call by reference/address):子程式收到同一份資料的位址/參照,內部改動會反映到外面。
- Python:形式參數會指向同一個物件(pass-by-object-reference)。不可變類型(int/float/str)重新賦值只影響局部;可變類型(list/dict)做 in-place 修改會影響外面。
def f(x):
x = x + 1
a = 5
f(a)
print(a) # 5
def g(L):
L.append(1)
nums = []
g(nums)
print(nums) # [1]參數傳遞的本質,是把資料流清楚地畫出來:資料由調用者送入子程式,再由回傳值送出結果。 當資料流清楚,子程式就容易被「搬到另一個程式」重用,亦容易用固定測試資料做驗證。
設計介面時,你要兼顧「夠用」與「易用」:參數不足會令子程式依賴外部狀態;參數太多會令調用容易出錯。 常見做法是把同類資料放進同一個陣列或字串中,減少參數數量,同時保持資料的結構性。
概念對照表:參數與變元
| 名詞 | 位置 | 例子 |
|---|---|---|
| 形式參數(formal parameter) | 子程式定義 | def area_circle(r): 的 r |
| 實際變元(actual argument) | 子程式調用 | area_circle(3) 的 3;或 area_circle(radius) 的 radius |
| 參數傳遞(parameter passing) | 調用當刻 | 把實際變元的值交給形式參數使用 |
延伸:按值調用/按址調用(Python 怎樣傳遞?)
DSE 常用「按值調用」(call by value)與「按址調用」(call by reference/address)來解釋:變元(argument)傳入子程式後,會唔會影響到主程式的同一個變量。
- 按值調用:子程式收到的是「值的副本」,子程式內改動不影響主程式。
- 按址調用:子程式收到的是「同一份資料的位址/參照」,子程式內改動會影響主程式。
- Python 的行為更準確叫
pass-by-object-reference(pass-by-assignment):形式參數會指向同一個物件。若物件是不可變(例如 int、float、str),在子程式內重新賦值只會改到「局部名稱」;若物件是可變(例如 list、dict),做 in-place 修改就會影響到外面。
def inc(x):
x = x + 1 # 重新賦值:只改到 inc 內的 x
a = 5
inc(a)
print(a) # 5(外面的 a 無變)
def add_one(lst):
lst.append(1) # in-place 修改:會改到同一個 list
nums = []
add_one(nums)
print(nums) # [1]
考試取向:你可以用「不可變類型 ≈ 按值調用」、「可變類型(in-place 修改)≈ 按址調用」作直觀理解,但要記住 Python 實際上係傳遞「物件參照」。
圖像化理解:資料流(Parameter → Function → Return)
你只需記住:形式參數是「入口名字」,實際變元是「真正給的資料」。
實作練習(Python)
練習 0(熱身):定義 ready()(無參數)
主程式已經寫好 ready()。請定義 ready()(無參數、無回傳值),令它輸出一行 READY。
顯示參考答案
def ready():
print("READY")
ready()練習 1(對應重點 1):power(base, exp)
讀入兩個整數(底數、指數),完成 power(base, exp) 回傳 base^exp,並輸出 ans=...。
(建議用迴圈;可不使用 **。)
顯示參考答案
def power(base, exp):
ans = 1
for _ in range(exp):
ans *= base
return ans
base = int(input())
exp = int(input())
print("ans=" + str(power(base, exp)))
練習 2(對應重點 2):次序對應(diff)
完成 diff(a, b) 回傳 a - b。留意:若把實際變元次序傳錯,答案會不同。
顯示參考答案
def diff(a, b):
return a - b
x = int(input())
y = int(input())
print("diff=" + str(diff(x, y)))
練習 3(對應重點 3):fahrenheit(c)(子程式只負責計算)
讀入攝氏溫度 c,完成 fahrenheit(c) 回傳華氏溫度。主流程負責輸出 F=...。
顯示參考答案
def fahrenheit(c):
return c * 9/5 + 32
c = float(input())
f = fahrenheit(c)
print("F=" + str(f))
練習 4(延伸):用一個參數接收「一組資料」(avg_list)
讀入一行整數(用空格分隔),把它們放入陣列,再把陣列傳入 avg_list(nums) 回傳平均值,輸出 avg=...。
顯示參考答案
def avg_list(nums):
return sum(nums) / len(nums)
line = input().strip()
nums = [int(x) for x in line.split()]
print("avg=" + str(avg_list(nums)))
def area_circle(r):
pi = 3.14159
return pi * r * r
print("面積:", area_circle(3))
子程式 area_circle(r)
pi ← 3.14159
回傳 pi × r × r
結束子程式
輸出 "面積:", area_circle(3)
Check Point(四選一)
ECCH4.4 回傳值與輸出:return 與 print 的分工
重點
- 回傳值(return)把結果交回調用位置;輸出(print)只負責顯示文字。
- 若你想把結果再用於計算或判斷,子程式必須回傳,而不是只輸出。
- 良好習慣:子程式做「計算/判斷」,主流程做「輸入/輸出格式」。
定義
- 回傳值:子程式把結果交回調用位置,調用者可把它儲存、再運算或再判斷。
- 輸出:把文字顯示到畫面;輸出本身通常不會把值交回程式。
- 副作用:子程式除了回傳值以外,還改變外界狀態(例如改全程變量、寫檔、輸出)。
原理/運作
- 例:
ans = square(7),執行完square後,調用位置得到回傳值並存入ans。 - 若子程式只
print而不return,調用位置通常得不到可用的數值(會是None)。
例子
若要把平均分同時用於「輸出」與「判斷等級」,回傳值特別重要:
def avg3(m1, m2, m3):
return (m1 + m2 + m3) / 3
a = avg3(72, 64, 80)
print("平均:", a)
if a >= 60:
print("合格")
比較
| 項目 | return(回傳) | print(輸出) |
|---|---|---|
| 目的 | 把結果交回程式 | 把文字顯示給使用者 |
| 能否再用於運算? | 可以(可存入變量) | 不可以(只是顯示) |
| 適合放在子程式內嗎? | 適合(計算型子程式) | 視乎設計;通常放主流程較清晰 |
常見錯誤
- 把「計算」寫成
print:結果無法在主流程再使用。 - 忘記把數值轉成字串:
"答案:" + ans會出錯(需str(ans))。 - 在
return後仍寫有用程式:return之後的語句不會執行。
初學者最常混淆的是:print 看起來「有顯示結果」,但它只是在畫面顯示,並沒有把結果交回程式使用。
若你希望主流程把結果再拿去做下一步(例如比較、累加、排序),就必須使用回傳值。
一個實用的分工原則是:子程式專注「計算/判斷」並回傳結果;主流程專注「輸入」與「輸出格式」。 這樣子程式更容易重用,亦更容易用測試資料驗證對錯。
圖像化理解:return 把值交回程式
記住:return 影響「程式」;print 影響「畫面」。
實作練習(Python)
練習 1(對應重點 1):sum2 必須 return,不要只 print
讀入兩個整數,完成 sum2 回傳和,主流程輸出 result=...。
顯示參考答案
def sum2(a, b):
return a + b
a = int(input())
b = int(input())
result = sum2(a, b)
print("result=" + str(result))
練習 2(對應重點 2):max3(a,b,c) 回傳最大值
讀入三個整數,完成 max3 並輸出 max=...。
顯示參考答案
def max3(a, b, c):
return max(a, b, c)
a = int(input())
b = int(input())
c = int(input())
print("max=" + str(max3(a, b, c)))
練習 3(對應重點 3):計算與顯示分工
完成 calc_total(price, qty)(回傳總價)與 show_total(total)(輸出 total=...)。
顯示參考答案
def calc_total(price, qty):
return price * qty
def show_total(total):
print("total=" + str(total))
price = float(input())
qty = int(input())
total = calc_total(price, qty)
show_total(total)
小測:四種子程式介面(寫程式)
以下 4 題分別練習:無參數/有參數 × 無回傳值/有回傳值。每題都要你「自己寫」子程式。
小測 1:無參數、無回傳值(Procedure)
定義一個子程式 show_welcome(),無參數、無回傳值,只輸出一行 Welcome!。主程式已經調用咗一次。
顯示參考答案
def show_welcome():
print("Welcome!")
show_welcome()小測 2:有參數、無回傳值(Procedure with parameters)
完成子程式 repeat_word(word, n):輸入一個字串 word 與整數 n(每行一個),輸出 word 共 n 行。
顯示參考答案
def repeat_word(word, n):
for _ in range(n):
print(word)
word = input()
n = int(input())
repeat_word(word, n)小測 3:無參數、有回傳值(Function without parameters)
完成函數 get_magic():無參數,但要 return 回傳整數 42。主程式會輸出 magic=...。
顯示參考答案
def get_magic():
return 42
print("magic=" + str(get_magic()))小測 4:有參數、有回傳值(Function with parameters)
完成函數 add3(a, b, c):回傳三個整數之和。主程式會讀入 3 個整數並輸出 sum=...。
顯示參考答案
def add3(a, b, c):
return a + b + c
a = int(input())
b = int(input())
c = int(input())
print("sum=" + str(add3(a, b, c)))def grade(mark):
if mark >= 80:
return "A"
elif mark >= 60:
return "B"
else:
return "C"
m = 72
print("等級:", grade(m))
子程式 grade(mark)
如果 mark ≥ 80 則
回傳 "A"
否則如果 mark ≥ 60 則
回傳 "B"
否則
回傳 "C"
結束如果
結束子程式
m ← 72
輸出 "等級:", grade(m)
Check Point(四選一)
ECCH4.5 範圍(Scope):局部變量與全程變量
重點
- 局部變量(local variable)只在子程式內有效;離開子程式後便不可再用。
- 全程變量(global variable)在程式多處可見,但過度使用會令依賴關係隱蔽,增加除錯難度。
- 優先用參數與回傳值傳遞資料;只有在確有需要時才使用全程變量。
定義
- 局部變量:在子程式內建立的變量,通常只在該子程式執行期間存在。
- 全程變量:在子程式外建立的變量,理論上可被多處程式讀取。
- 遮蔽:若子程式內使用與全程變量同名的局部變量,局部變量會暫時遮住外面的同名變量。
原理/運作
- 在子程式內讀取全程變量一般沒有問題。
- 若要在子程式內改寫全程變量的值,Python 需要使用
global聲明,否則會被當作建立新的局部變量。
count = 0
def add_one():
global count
count = count + 1
add_one()
print(count) # 1
例子
把資料當作參數傳入,再回傳新值,資料流會更清晰:
def add_to_total(total, x):
return total + x
total = 0
total = add_to_total(total, 5)
total = add_to_total(total, 7)
print(total)
比較
- 用參數/回傳值:資料流清晰,子程式較易獨立測試。
- 用全程變量:調用時看不出依賴,容易在多處被改寫,錯誤較難追查。
常見錯誤
- 以為子程式內同名變量一定是全程變量:其實多數情況是局部變量。
- 濫用全程變量作暫存:令子程式互相干擾,行為不穩定。
- 把「修改可變資料結構內容」與「改寫變量指向」混淆:兩者影響範圍不同。
範圍(scope)決定了變量「在哪裡可用」。理解局部變量與全程變量的差別,可以避免大量「看似相同、其實不是同一個」的錯誤。 實作時,你可以把子程式當作一個小型機器:外界透過參數把材料送入,機器運作後用回傳值交回結果。
若你發現自己需要不斷依靠全程變量來共享資料,通常表示介面仍可改善:把需要的資料變成參數,或把結果以回傳值交回。 這會讓程式更穩定、更容易測試與維護。
概念對照表:局部變量 vs 全程變量
| 比較項目 | 局部變量(local variable) | 全程變量(global variable) |
|---|---|---|
| 可見範圍 | 只在子程式內 | 多處可見(整個程式) |
| 生命期 | 子程式執行期間 | 程式執行期間 |
| 主要用途 | 子程式內部暫存、計算 | 真正需要共享狀態(慎用) |
| 風險 | 低(影響範圍小) | 高(依賴隱蔽、難除錯) |
圖像化理解:Scope 的「盒子」
理解重點:局部變量只在內層盒子有效;全程變量在外層盒子有效。
實作練習(Python)
練習 1(對應重點 1):triple(x) 使用局部變量再回傳
讀入整數 x,在子程式內用局部變量計算 3 倍,輸出 y=...。
顯示參考答案
def triple(x):
y = x * 3
return y
x = int(input())
y = triple(x)
print("y=" + str(y))
練習 2(對應重點 2):在子程式內改寫全程變量(global)
完成 add_one(),令它每次被調用都把全程變量 count 加 1。
顯示參考答案
count = 0
def add_one():
global count
count = count + 1
add_one()
add_one()
print("count=" + str(count))
練習 3(對應重點 3):用參數/回傳值避免全程變量
完成 add_to_total(total, x) 回傳新總和,最後輸出 total=...。
顯示參考答案
def add_to_total(total, x):
return total + x
total = 0
total = add_to_total(total, 5)
total = add_to_total(total, 7)
print("total=" + str(total))
total = 0 # 全程變量
def add_to_total(x):
global total
total += x
add_to_total(5)
add_to_total(7)
print("total =", total)
total ← 0 (全程變量)
子程式 add_to_total(x)
total ← total + x
結束子程式
add_to_total(5)
add_to_total(7)
輸出 "total =", total
Check Point(四選一)
ECCH4.6 程式測試和除錯:由子程式開始驗證
重點
- 先測子程式,再測整體:把問題範圍縮小,除錯效率更高。
- 使用空運行(dry run)與追蹤表(trace table),可清楚看見變量如何改變。
- 除錯時優先確認:輸入是否正確、算法是否正確、輸出格式是否符合要求。
定義
- 測試:用不同輸入驗證程式行為是否符合預期。
- 除錯(debug):找出造成錯誤的原因並修正。
- 追蹤表:在空運行時記錄每一步變量值,以分析邏輯。
原理/運作
- 先為子程式準備簡單測試資料(例如固定參數),確認回傳值正確。
- 再加入邊界情況(例如 0、負數、空字串、空陣列)。
- 最後才把子程式接回主流程,測整體輸入與輸出。
由小到大的方法,可以把錯誤定位在某一個子程式,而不是在整段程式亂猜。
例子
def sum_to_n(n):
s = 0
for i in range(1, n + 1):
s = s + i
return s
空運行 n = 4 時:
| i | s(更新後) |
|---|---|
| 1 | 1 |
| 2 | 3 |
| 3 | 6 |
| 4 | 10 |
比較
- 只測整體:一出錯就要在整段程式找原因,範圍大、效率低。
- 先測子程式:逐個模組驗證,較快鎖定出錯位置。
常見錯誤
- 只測一個例子:程式在其他輸入下可能失敗。
- 測試沒有預期答案:不知對錯,也無法有效除錯。
- 加入大量輸出而不整理:輸出過多反而看不出關鍵。
子程式特別適合做系統化測試,因為它的輸入與輸出較清晰:你可以直接指定參數,觀察回傳值是否正確。 當子程式通過測試後,再把它放回主流程,整體錯誤的可能性便會大幅下降。
除錯不是碰運氣,而是有步驟的分析:先閱讀錯誤訊息(行號與類型),再用追蹤表找出「哪一步開始偏離預期」, 最後針對該步驟或子程式修正。
除錯清單(可操作)
| 先檢查 | 你可以做甚麼 | 常見例子 |
|---|---|---|
| 輸入 | 確保讀入了正確數量/類型(int/float/str) | 把 input() 讀到的資料用 print() 暫時輸出驗證 |
| 算法 | 空運行、列追蹤表、檢查迴圈範圍 | range(1, n) 少了 n;或邊界條件寫錯 |
| 輸出格式 | 對照題目要求(大小寫、空格、換行) | 多了一個空格或少了一行,導致核對失敗 |
實作練習(Python)
練習 1(對應重點 1):先測子程式(內置測試)
完成 last_digit(n) 回傳個位數。下面已寫好測試;當全部正確時會輸出 OK。
顯示參考答案
def last_digit(n):
return n % 10
tests = [(507, 7), (12, 2), (0, 0)]
all_ok = True
for n, expected in tests:
got = last_digit(n)
if got != expected:
all_ok = False
print("OK" if all_ok else "NOT OK")
練習 2(對應重點 2):用輸出模擬追蹤表(trace_sum_to_n)
讀入整數 n(建議 1–5),每次迴圈都輸出 i=..., s=...,最後輸出 total=...。
顯示參考答案
def trace_sum_to_n(n):
s = 0
for i in range(1, n + 1):
s += i
print("i=" + str(i) + ", s=" + str(s))
return s
n = int(input())
total = trace_sum_to_n(n)
print("total=" + str(total))
練習 3(對應重點 3):修正 off-by-one(由測試找錯)
下面的 sum_to_n 有錯:它少加了最後一項。請修正,令測試輸出 OK。
顯示參考答案
def sum_to_n(n):
s = 0
for i in range(1, n + 1):
s += i
return s
tests = [(1, 1), (4, 10), (10, 55)]
all_ok = True
for n, expected in tests:
got = sum_to_n(n)
if got != expected:
all_ok = False
print("OK" if all_ok else "NOT OK")
子程式 sum_to_n(n)
s ← 0
對於 i ← 1 至 n
s ← s + i
結束對於
回傳 s
結束子程式
def sum_to_n(n):
s = 0
for i in range(1, n + 1):
s = s + i
return s
Check Point(四選一)
ECCH4.7 重構與抽象:把重複變成可維護
重點
- 重構是「改善結構但不改變功能」:令程式更清晰、更易維護。
- 常見重構方法:把重複片段抽成子程式,並以參數表達差異。
- 重構後應重新測試,確保輸入輸出行為與原本一致。
定義
- 重構:不改變程式外在行為的前提下,改善程式內部結構。
- 抽象:抓住共同特徵,把細節隱藏在子程式內,讓外部只看到「做甚麼」。
- 參數化:把差異變成參數,避免為每個小變化都複製一份程式。
原理/運作
- 找出重複片段(邏輯相同或非常相似)。
- 把共通部分抽成子程式。
- 把不同之處變成參數(例如不同訊息、不同次數、不同折扣率)。
- 替換原本重複片段為子程式調用。
例子
# 重構前(重複)
print("Hello, Amy")
print("Hello, Ben")
print("Hello, Chris")
# 重構後(抽象 + 參數化)
def hello(name):
print("Hello, " + name)
hello("Amy")
hello("Ben")
hello("Chris")
比較
- 未重構:同一改動要改多處;容易漏改,造成不一致。
- 重構後:改動集中在子程式;所有調用位置自動受惠。
常見錯誤
- 把不相干的事硬塞成同一子程式,導致參數越來越多、越來越難用。
- 重構後不再測試:改壞了輸出格式而不自知。
- 只重構表面:例如只改名但不改善結構,問題仍在。
當程式開始變長,重複往往是最早出現的警號:同一段計算、同一段輸出、同一段檢查,不斷在不同地方出現。 這會令修改成本變高,因為你必須在多處同步更新。
重構的目標是讓「改動只需改一處」。抽成子程式後,你只要在子程式內修正一次,所有調用位置都會自動受惠。 這亦是大型程式能夠被長期維護的關鍵能力。
重構前後對照(表格)
| 情況 | 重構前 | 重構後 |
|---|---|---|
| 同一訊息要改內容 | 要改多處 print |
只改子程式一次 |
| 要加新名字 | 再複製一行/一段 | 調用同一子程式,維持一致格式 |
圖像化理解:重構的步驟
重構的終點不是「看起來更短」,而是「更易維護、較少出錯」。
實作練習(Python)
練習 1(對應重點 1):用 greet(name) 取代重複
完成 greet,令它輸出 Hello, <name>,然後用列表調用三次。
顯示參考答案
def greet(name):
print("Hello, " + name)
names = ["Amy", "Ben", "Chris"]
for n in names:
greet(n)
練習 2(對應重點 2):把差異變成參數(print_border)
完成 print_border(ch, n),令它輸出由 ch 重覆 n 次組成的一行。
顯示參考答案
def print_border(ch, n):
print(ch * n)
print_border("*", 5)
print_border("-", 8)
練習 3(對應重點 3):重構後要重測(fee)
完成 fee(age):若 age < 12 回傳 5;若 age < 65 回傳 10;否則回傳 6。
測試已寫好,成功會輸出 OK。
顯示參考答案
def fee(age):
if age < 12:
return 5
elif age < 65:
return 10
else:
return 6
tests = [(8, 5), (30, 10), (70, 6)]
all_ok = True
for age, expected in tests:
if fee(age) != expected:
all_ok = False
print("OK" if all_ok else "NOT OK")
def print_border(ch, n):
print(ch * n)
print_border("*", 10)
print("標題")
print_border("*", 10)
子程式 print_border(ch, n)
輸出 ch 重覆 n 次
結束子程式
print_border("*", 10)
輸出 "標題"
print_border("*", 10)
Check Point(四選一)
ECCH4.8 遞歸(recursion,延伸):子程式調用自己
重點
- 遞歸(recursion):子程式在其內部調用自己,以解決「結構相似」的問題。
- 遞歸必須有終止條件(base case),否則會無限調用而導致錯誤。
- 遞歸依賴堆疊(stack)保存返回位置;理解「先進後出」能幫助你追蹤遞歸流程。
定義
- 遞歸:子程式以更小的同類問題調用自己,直到達到終止條件。
- 終止條件:不用再分解、可直接給答案的最小情況。
- 遞歸關係:把大問題轉成小問題的規則。
原理/運作
- 每一次調用都會把目前狀態放入堆疊,等待子程式完成後再返回。
- 最深層先完成(符合先進後出),然後逐層返回並組合答案。
理解方式:先找終止條件,再看遞歸如何把問題縮小,最後看返回時如何合併結果。
例子
def fact(n):
if n == 0:
return 1
return n * fact(n - 1)
print(fact(5)) # 120
比較
- 遞歸:寫法貼近數學定義,對樹狀問題(例如遍歷)較自然。
- 迭代(iteration):通常較省堆疊空間,亦較易避免深度過大。
常見錯誤
- 缺少終止條件或終止條件寫錯。
- 遞歸沒有縮小問題(例如
fact(n)又調用fact(n))。 - 忽略輸入限制:過大的
n可能造成遞歸深度過深。
遞歸是子程式概念的一個自然延伸:既然子程式可以調用其他子程式,也可以調用自己。 不過,遞歸的正確性非常依賴終止條件與「縮小問題」的設計;只要其中一點出錯,就會出現無限調用。
建議你用小數值做空運行:先描述「調用一路往下」的參數變化,再描述「返回一路往上」如何組合答案。 一旦能清楚說出這兩段流程,你便真正掌握了遞歸。
圖像化理解:遞歸堆疊(以 factorial 為例)
先進後出:最深層(終止條件)先回傳,然後逐層返回並計算。
實作練習(Python)
練習 1(對應重點 1):fact(n)(遞歸)
讀入整數 n,完成遞歸階乘,輸出 fact=...。
顯示參考答案
def fact(n):
if n == 0:
return 1
return n * fact(n - 1)
n = int(input())
print("fact=" + str(fact(n)))
練習 2(對應重點 2):sum_digits(n) 必須有終止條件
讀入整數 n,回傳各位數字之和,輸出 ans=...。
顯示參考答案
def sum_digits(n):
if n < 10:
return n
return (n % 10) + sum_digits(n // 10)
n = int(input())
print("ans=" + str(sum_digits(n)))
練習 3(對應重點 3):用 enter/leave 觀察堆疊
讀入整數 n(建議 1–5)。寫遞歸子程式 countdown(n):
進入一層時輸出 enter n,離開一層時輸出 leave n。
顯示參考答案
def countdown(n):
print("enter " + str(n))
if n == 0:
print("leave " + str(n))
return
countdown(n - 1)
print("leave " + str(n))
n = int(input())
countdown(n)
def sum_digits(n):
if n < 10:
return n
return (n % 10) + sum_digits(n // 10)
print(sum_digits(507)) # 12
子程式 sum_digits(n)
如果 n < 10 則
回傳 n
結束如果
回傳 (n MOD 10) + sum_digits(n DIV 10)
結束子程式
輸出 sum_digits(507)
Check Point(四選一)
ECCH4.9 20 條挑戰題:偽代碼 ⇄ Python(子程式綜合練習)
使用方法
- 每題都有偽代碼(理解算法)與 Python 編輯器(實作)。
- 完成後可按 Run 程式 測試;部分題目提供 核對答案(隱藏測試)。
- 如遇到錯誤,先閱讀錯誤訊息,再用追蹤表或加入少量輸出逐步定位。
提示:為避免輸出格式影響核對,請盡量按題目要求輸出指定文字。