選修單元 C:子程式(ECCH4)

本頁以「由拆解到實作」的方式學習子程式:先理解模組性與介面(參數、回傳值),再延伸到範圍(scope)、 程式測試和除錯、重構與抽象,最後以 20 條偽代碼挑戰題作綜合練習。

本章你應該能做到

ECCH4.1 模組性與拆解:為何要寫子程式?

重點

寫子程式不是為了「把程式切開」而已,而是為了令程式的結構更清晰:主流程負責描述整體步驟, 子程式負責完成某個小任務。當你能把任務拆到「每一段都可以用一句話說明」時,整體程式通常就容易理解。

拆解亦能提升維護能力:如果同一段計算在多處出現,將它封裝成子程式後,將來改規則只需改一處。 對於較大型的程式,這種「改動集中、影響一致」的特性非常重要。

圖像化理解:結構圖(Structure Chart)

結構圖:主流程與子程式 主流程(Main Flow) 讀取輸入 read_input() 處理計算 calc_average() 格式化輸出 show_result()

觀察重點:主流程只描述「做事順序」,每個子程式只負責一項任務。這樣寫出來的程式較容易閱讀與測試。

拆解思考表

你可以問自己目的例子
這段程式在其他地方會再用嗎?找出可重用模組計算折扣、檢查合格、格式化輸出
這段程式能否用一句話說明?確保子程式責任清晰「計算平均分」比「處理資料」更清楚
外界需要給它甚麼資料?決定參數半徑 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))

Check Point(四選一)

請按「下一題」開始。

ECCH4.2 子程式的定義與調用:由「寫一次」到「用多次」

重點

在設計子程式時,先用一句話寫下「它要做甚麼」,再把這句話濃縮成恰當的名稱。 例如「把攝氏轉華氏」就很適合叫 fahrenheit(c);名稱越清楚,主流程就越像在閱讀一份步驟說明。

另外,子程式最好做到「輸入清楚、輸出清楚」:需要的資料由參數傳入,計算結果以回傳值交回。 若把輸入與輸出全部寫進子程式內,會令子程式難以重用,亦不便測試。

圖像化理解:調用 → 執行 → 返回

調用流程:主流程跳到子程式,再回到下一行 主流程 ... 調用 f(x) ... 子程式 f(x) 計算/處理/回傳 跳轉(call) 返回(return) 主流程(下一行) ... 使用回傳值/繼續執行 ...

調用就像「去做一件事」:做完後會回到原來位置的下一行;若有回傳值,主流程可以把它當作普通數值使用。

概念對照表:定義與調用

概念你在程式中看到的樣子作用
定義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()

Check Point(四選一)

請按「下一題」開始。

ECCH4.3 參數傳遞與介面設計:形式參數、實際變元

重點

參數傳遞的本質,是把資料流清楚地畫出來:資料由調用者送入子程式,再由回傳值送出結果。 當資料流清楚,子程式就容易被「搬到另一個程式」重用,亦容易用固定測試資料做驗證。

設計介面時,你要兼顧「夠用」與「易用」:參數不足會令子程式依賴外部狀態;參數太多會令調用容易出錯。 常見做法是把同類資料放進同一個陣列或字串中,減少參數數量,同時保持資料的結構性。

概念對照表:參數與變元

名詞位置例子
形式參數(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)

資料流:實際變元傳入形式參數,回傳值交回主流程 調用位置 area_circle(3) 實際變元:3 子程式內 形式參數:r 計算:pi * r * r 回到調用位置 得到回傳值 例如:28.27431

你只需記住:形式參數是「入口名字」,實際變元是「真正給的資料」。

實作練習(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))

Check Point(四選一)

請按「下一題」開始。

ECCH4.4 回傳值與輸出:return 與 print 的分工

重點

初學者最常混淆的是:print 看起來「有顯示結果」,但它只是在畫面顯示,並沒有把結果交回程式使用。 若你希望主流程把結果再拿去做下一步(例如比較、累加、排序),就必須使用回傳值。

一個實用的分工原則是:子程式專注「計算/判斷」並回傳結果;主流程專注「輸入」與「輸出格式」。 這樣子程式更容易重用,亦更容易用測試資料驗證對錯。

圖像化理解:return 把值交回程式

return 把值交回主流程,print 只顯示 子程式 計算後 return 結果 主流程(程式) 可以把結果存入變量再用 畫面(輸出) print 只把文字顯示 return print

記住: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(每行一個),輸出 wordn 行。

顯示參考答案
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))

Check Point(四選一)

請按「下一題」開始。

ECCH4.5 範圍(Scope):局部變量與全程變量

重點

範圍(scope)決定了變量「在哪裡可用」。理解局部變量與全程變量的差別,可以避免大量「看似相同、其實不是同一個」的錯誤。 實作時,你可以把子程式當作一個小型機器:外界透過參數把材料送入,機器運作後用回傳值交回結果。

若你發現自己需要不斷依靠全程變量來共享資料,通常表示介面仍可改善:把需要的資料變成參數,或把結果以回傳值交回。 這會讓程式更穩定、更容易測試與維護。

概念對照表:局部變量 vs 全程變量

比較項目局部變量(local variable)全程變量(global variable)
可見範圍只在子程式內多處可見(整個程式)
生命期子程式執行期間程式執行期間
主要用途子程式內部暫存、計算真正需要共享狀態(慎用)
風險低(影響範圍小)高(依賴隱蔽、難除錯)

圖像化理解:Scope 的「盒子」

Scope 示意:全程範圍包住子程式範圍 全程範圍(global scope) 例:total、count 子程式範圍(local scope) 例:在子程式內建立的 temp、i、s ... 離開子程式後,局部變量一般不再可用

理解重點:局部變量只在內層盒子有效;全程變量在外層盒子有效。

實作練習(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)

Check Point(四選一)

請按「下一題」開始。

ECCH4.6 程式測試和除錯:由子程式開始驗證

重點

子程式特別適合做系統化測試,因為它的輸入與輸出較清晰:你可以直接指定參數,觀察回傳值是否正確。 當子程式通過測試後,再把它放回主流程,整體錯誤的可能性便會大幅下降。

除錯不是碰運氣,而是有步驟的分析:先閱讀錯誤訊息(行號與類型),再用追蹤表找出「哪一步開始偏離預期」, 最後針對該步驟或子程式修正。

除錯清單(可操作)

先檢查你可以做甚麼常見例子
輸入確保讀入了正確數量/類型(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
結束子程式

Check Point(四選一)

請按「下一題」開始。

ECCH4.7 重構與抽象:把重複變成可維護

重點

當程式開始變長,重複往往是最早出現的警號:同一段計算、同一段輸出、同一段檢查,不斷在不同地方出現。 這會令修改成本變高,因為你必須在多處同步更新。

重構的目標是讓「改動只需改一處」。抽成子程式後,你只要在子程式內修正一次,所有調用位置都會自動受惠。 這亦是大型程式能夠被長期維護的關鍵能力。

重構前後對照(表格)

情況重構前重構後
同一訊息要改內容 要改多處 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)

Check Point(四選一)

請按「下一題」開始。

ECCH4.8 遞歸(recursion,延伸):子程式調用自己

重點

遞歸是子程式概念的一個自然延伸:既然子程式可以調用其他子程式,也可以調用自己。 不過,遞歸的正確性非常依賴終止條件與「縮小問題」的設計;只要其中一點出錯,就會出現無限調用。

建議你用小數值做空運行:先描述「調用一路往下」的參數變化,再描述「返回一路往上」如何組合答案。 一旦能清楚說出這兩段流程,你便真正掌握了遞歸。

圖像化理解:遞歸堆疊(以 factorial 為例)

遞歸堆疊:fact(3) → fact(2) → fact(1) → fact(0) 堆疊(stack) fact(3) 等待 fact(2) fact(2) 等待 fact(1) fact(1) 等待 fact(0) fact(0) 回傳 1(終止) 返回時逐層組合 fact(1) = 1 × 1 fact(2) = 2 × 1 fact(3) = 3 × 2 結果:6

先進後出:最深層(終止條件)先回傳,然後逐層返回並計算。

實作練習(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

Check Point(四選一)

請按「下一題」開始。

ECCH4.9 20 條挑戰題:偽代碼 ⇄ Python(子程式綜合練習)

使用方法

  • 每題都有偽代碼(理解算法)與 Python 編輯器(實作)。
  • 完成後可按 Run 程式 測試;部分題目提供 核對答案(隱藏測試)。
  • 如遇到錯誤,先閱讀錯誤訊息,再用追蹤表或加入少量輸出逐步定位。

提示:為避免輸出格式影響核對,請盡量按題目要求輸出指定文字。