Home
探索 Uedu
學生控制台
註冊會員/登入
研究知情同意中心
教師控制台
課程設定
支援與訊息
Uptime 數據

UeduGPTs

--

Jupyters

4

UG26 CISOSE26
臺北 AQI 46 · 臺中 AQI 28 · 臺南 AQI 24 · 高雄 AQI 33

AI 回覆桌面通知

AI 助教回覆完成時顯示桌面通知

聊天訊息通知

同學在討論區發送訊息時通知

聲音通知

每當有新通知時播放提示音

流程控制

Python 流程控制:讓程式替你做決定與重複做事

從一份成績單出發,學會 if/elif/else、布林短路、for 與 while、break/continue、巢狀迴圈與列表推導式,並一窺 for 背後的迭代器協定。

從一份成績單開始:讓程式替你「做決定」與「重複做事」

假設你拿到一個班級的小考分數,想做三件事:把不及格的同學挑出來、算出全班平均、再把每位同學標上等第。如果只用前面學過的變數與運算,你會發現少了兩樣關鍵能力:判斷(依分數走不同的路)與重複(對每位同學做同樣的事)。

這正是「流程控制(control flow)」要解決的事。程式預設是「由上往下、一行接一行」執行的,但真實任務幾乎都需要分岔與循環。本文就帶你把這份成績單從頭跑出來,過程中把 Python 的判斷與迴圈一次學齊。先看一段最小的雛形:

scores = [88, 56, 73, 95, 42, 67]

total = 0
for s in scores:           # 重複:對每個分數做一次
    total += s
average = total / len(scores)

for s in scores:
    if s < 60:             # 判斷:不及格的才印
        print(f"{s} 不及格")

print(f"平均:{average}")
# 輸出:
# 56 不及格
# 42 不及格
# 平均:70.16666666666667

短短幾行,已經同時用到了「判斷」與「重複」。接下來我們把每個零件拆開,逐一講清楚。

流程控制概念示意圖

if / elif / else:讓程式走不同的路

最基本的判斷是 if:當條件成立(為真)時,才執行縮排底下的區塊。Python 用縮排(慣例為 4 個空格)來界定區塊,而不是大括號。

score = 73

if score >= 60:
    print("及格")
# 輸出:及格

如果要在「不成立」時做另一件事,加上 else;若有多種互斥情況,用 elif(else if 的縮寫)串接。Python 會由上往下檢查,命中第一個為真的分支後就跳出整串,不會再檢查後面的條件:

def grade(score):
    if score >= 90:
        return "A"
    elif score >= 80:
        return "B"
    elif score >= 70:
        return "C"
    elif score >= 60:
        return "D"
    else:
        return "F"

print(grade(95))   # 輸出:A
print(grade(73))   # 輸出:C
print(grade(42))   # 輸出:F

注意條件的順序很重要。因為命中即停,所以要把較嚴格(範圍較小)的條件放前面。如果你把 score >= 60 放到最上面,那麼 95 分也會先命中它而被判成 "D",這是初學者常見的邏輯錯誤。

比較與布林值

條件運算式的結果是布林值(bool),只有 TrueFalse 兩種。常用的比較運算子有 ==(相等)、!=(不相等)、<<=>>=

特別提醒:判斷相等用兩個等號 ==,單一等號 = 是「賦值」。把 if score = 60 寫成這樣會直接語法錯誤,這其實是 Python 替你擋掉的一個常見地雷。

Python 還支援數學式的連續比較,相當直覺:

score = 75
if 70 <= score < 80:        # 等同 70 <= score and score < 80
    print("這是 C 段")
# 輸出:這是 C 段

布林運算與短路求值:and、or、not

要組合多個條件,用 and(且)、or(或)、not(否)。

age = 20
has_id = True

if age >= 18 and has_id:
    print("可以借書")
# 輸出:可以借書

這裡有一個容易被忽略但很重要的機制:短路求值(short-circuit evaluation)

  • A and B:如果 A 已經是假,整體必為假,Python 不會再去算 B
  • A or B:如果 A 已經是真,整體必為真,Python 不會再去算 B

短路不只是省一點計算,更是一種防護寫法。看這個例子:

data = []

# ✅ 正確:先確認非空,才存取第一個元素
if data and data[0] > 0:
    print("第一個是正數")
else:
    print("空的或非正數")
# 輸出:空的或非正數

因為 data 是空串列,在布林情境下為假,and 短路後就不會去算 data[0],因此不會引發「索引超出範圍」的錯誤。如果把順序寫反成 data[0] > 0 and data,空串列時就會直接爆炸。善用短路順序,能讓程式更穩健。

順帶一提「真假值(truthiness)」:在條件中,空字串 ""、空串列 []0None 都會被當作假,非空與非零則為真。所以判斷串列非空時,慣例是直接寫 if data:,而不是 if len(data) > 0:,後者雖然正確但較囉嗦,不符合 Python 風格。

for 與 range:走訪每一個元素

for 迴圈用來「對序列中的每個元素各做一次」。Python 的 for走訪導向的,直接拿到元素本身,不需要手動維護索引:

fruits = ["蘋果", "香蕉", "芭樂"]

for fruit in fruits:
    print(f"我喜歡{fruit}")
# 輸出:
# 我喜歡蘋果
# 我喜歡香蕉
# 我喜歡芭樂

當你需要「重複固定次數」時,用 range() 產生一串整數。range(n) 是從 0 到 n−1(不含 n):

for i in range(5):
    print(i, end=" ")
print()
# 輸出:0 1 2 3 4

range 也接受起點、終點與步長:range(start, stop, step),同樣是「含頭不含尾」:

for i in range(2, 11, 2):   # 從 2 開始,每次 +2,到 11 前停
    print(i, end=" ")
print()
# 輸出:2 4 6 8 10

如果你同時需要「索引」與「元素」,不要自己寫 range(len(...)) 再去取值,那是 C 語言式的反模式。Python 的慣例是用 enumerate()

fruits = ["蘋果", "香蕉", "芭樂"]

# ❌ 反模式:囉嗦又容易出錯
for i in range(len(fruits)):
    print(i, fruits[i])

# ✅ 慣例寫法
for i, fruit in enumerate(fruits):
    print(i, fruit)
# 輸出:
# 0 蘋果
# 1 香蕉
# 2 芭樂

若要同時走訪兩個等長序列,用 zip() 把它們配對:

names = ["小明", "小華", "小美"]
scores = [88, 56, 95]

for name, score in zip(names, scores):
    print(f"{name}:{score} 分")
# 輸出:
# 小明:88 分
# 小華:56 分
# 小美:95 分

while:直到條件不成立才停

當你不知道要重複幾次,只知道「停止條件」時,用 while。它會在每一輪開始前檢查條件,只要為真就繼續:

balance = 100
month = 0

while balance > 0:
    balance -= 30          # 每月扣 30
    month += 1

print(f"撐了 {month} 個月")
# 輸出:撐了 4 個月

使用 while 一定要確保條件終究會變成假,否則就是無窮迴圈。最常見的雷是「忘記更新條件變數」:

# ❌ 危險:i 永遠是 0,永遠不會停
i = 0
while i < 5:
    print(i)
    # 忘了寫 i += 1

while 時養成習慣:迴圈內一定要有「往終止方向推進」的那一行。

break 與 continue:中途跳出或跳過

兩個關鍵字能更精細地控制迴圈:

  • break立刻結束整個迴圈。
  • continue跳過本輪剩下的程式,直接進入下一輪。
# 找出第一個及格的分數就停
scores = [42, 55, 73, 95, 60]

for s in scores:
    if s >= 60:
        print(f"找到第一個及格:{s}")
        break
# 輸出:找到第一個及格:73
# 印出 1 到 10 之間的奇數(用 continue 跳過偶數)
for n in range(1, 11):
    if n % 2 == 0:
        continue           # 是偶數,跳過底下的 print
    print(n, end=" ")
print()
# 輸出:1 3 5 7 9

Python 還有個少見但好用的語法:for ... elseelse 區塊只在迴圈沒有被 break 中斷、自然跑完時執行,很適合表達「找遍了都沒找到」:

target = 100
scores = [42, 55, 73, 95, 60]

for s in scores:
    if s == target:
        print("找到了")
        break
else:
    print("整份名單都沒有這個分數")
# 輸出:整份名單都沒有這個分數

巢狀迴圈:迴圈裡面還有迴圈

當資料有兩個維度(例如「每個班 × 每位學生」),就會用到巢狀迴圈。外層每跑一輪,內層就完整跑一遍。經典例子是九九乘法表:

for i in range(1, 4):
    for j in range(1, 4):
        print(f"{i}x{j}={i*j}", end="  ")
    print()                # 每跑完一個 i 就換行
# 輸出:
# 1x1=1  1x2=2  1x3=3
# 2x1=2  2x2=4  2x3=6
# 3x1=3  3x2=6  3x3=9

要留意的是巢狀迴圈的成本:外層 $n$ 次、內層 $m$ 次,總共執行 $n \times m$ 次。若兩層都是 $n$,複雜度就是 $O(n^2)$。資料一大,巢狀迴圈很容易變慢,這是日後優化的重點觀察處。

另外,break 只會跳出它所在的那一層迴圈,不會一次跳出全部。若真的需要從內層直接結束外層,常見做法是包成函式用 return,或設一個旗標(flag)變數。

列表推導式:用一行表達「轉換」與「篩選」

很多迴圈的本質只是「拿一個串列,逐一加工後產生新串列」。Python 為這種模式提供了簡潔的列表推導式(list comprehension)

先看傳統寫法與推導式的對照:

nums = [1, 2, 3, 4, 5]

# 傳統迴圈:把每個數平方
squares = []
for n in nums:
    squares.append(n ** 2)
print(squares)             # 輸出:[1, 4, 9, 16, 25]

# 列表推導式:同樣的事,一行搞定
squares = [n ** 2 for n in nums]
print(squares)             # 輸出:[1, 4, 9, 16, 25]

推導式也能加上 if 來篩選。語法是 [運算式 for 元素 in 序列 if 條件]

nums = range(1, 11)
evens = [n for n in nums if n % 2 == 0]
print(evens)              # 輸出:[2, 4, 6, 8, 10]

回到開頭的成績單,挑出所有及格分數只要一行:

scores = [88, 56, 73, 95, 42, 67]
passed = [s for s in scores if s >= 60]
print(passed)             # 輸出:[88, 73, 95, 67]

動手寫一段:成績單一次跑完

把前面學的判斷、迴圈、推導式全部串起來,完成本文開頭設定的三個任務:

names = ["小明", "小華", "小美", "阿傑", "小芳", "大雄"]
scores = [88, 56, 73, 95, 42, 67]

def grade(score):
    if score >= 90:
        return "A"
    elif score >= 80:
        return "B"
    elif score >= 70:
        return "C"
    elif score >= 60:
        return "D"
    else:
        return "F"

# 任務 1:挑出不及格的人
failed = [name for name, s in zip(names, scores) if s < 60]
print("不及格名單:", failed)

# 任務 2:算平均
average = sum(scores) / len(scores)
print(f"全班平均:{average:.1f}")

# 任務 3:標等第
print("等第:")
for name, s in zip(names, scores):
    print(f"  {name}:{s} 分 → {grade(s)}")

# 輸出:
# 不及格名單: ['小華', '小芳']
# 全班平均:70.2
# 等第:
#   小明:88 分 → B
#   小華:56 分 → F
#   小美:73 分 → C
#   阿傑:95 分 → A
#   小芳:42 分 → F
#   大雄:67 分 → D

短短二十幾行,你已經寫出一支實用的小程式。試著改改看:加上「列出全班最高分的人」或「只列出 A 與 B 段」,鞏固今天學到的工具。

常見錯誤與重點回顧

  • === 搞混:賦值用 =,判斷相等用 ==。在 if 條件裡寫成 = 會直接語法錯誤。
  • elif 條件順序排反:因為「命中即停」,較嚴格的條件要放前面。把 >= 60 放最上面,會讓 95 分也被歸進最低門檻那一段。
  • while 忘記更新條件變數:迴圈內一定要有「往終止方向推進」的那一行,否則無窮迴圈。
  • range(len(x)) 取索引:要索引請用 enumerate(x),要配對兩個序列請用 zip(),不要自己手動算索引。
  • 誤以為 break 能跳出所有層break 只跳出當前那一層巢狀迴圈,不是全部。
  • 判斷空容器寫 len(x) > 0:Python 慣例直接寫 if x:,更簡潔也更符合風格。

深入探討(研究所視角)

推導式 vs 迴圈:可讀性與效能的權衡

列表推導式不只是「比較短」,它在 CPython 中通常也比等價的 for + append 更快。原因是:明寫的迴圈每一輪都要在 Python 層級查找並呼叫 list.append 這個方法(涉及屬性查找與函式呼叫開銷),而推導式由直譯器以專用的位元碼(如 LIST_APPEND)在內部完成,省去了反覆的方法查找。

但「能用推導式」不等於「應該用」。推導式的甜蜜點是單純的轉換與篩選。一旦邏輯變複雜——例如需要多重巢狀、夾帶副作用(印東西、寫檔)、或條件分支太多——硬塞進一行反而傷害可讀性。下面是一個被濫用的反例:

# ❌ 可讀性災難:巢狀加多重條件,難以一眼看懂
result = [f(x, y) for x in xs if g(x) for y in ys if h(x, y) if x != y]

這種情況改回明確的 for 迴圈,搭配適當的變數命名與註解,對維護者更友善。一個實用準則:如果推導式需要讀第二遍才懂,就拆成迴圈。此外,若你只是要「逐一處理而不需要產生新串列」(純副作用),請直接用 for,不要為了炫技寫成推導式再丟棄結果。

值得一提的是,當資料量很大且只需逐一消費時,可改用產生器運算式(generator expression),把 [] 換成 ()

total = sum(n ** 2 for n in range(1_000_000))

它不會一次把整個串列建在記憶體裡,而是邊算邊吐,記憶體佔用是 $O(1)$ 而非 $O(n)$,對大資料尤其關鍵。

for 的背後:迭代器協定(iterator protocol)

for 看似魔法,其實建立在一個明確的契約——迭代器協定之上。當你寫 for x in obj 時,Python 實際做了這些事:

  1. 先呼叫 iter(obj),也就是 obj.__iter__(),取得一個迭代器(iterator)
  2. 不斷呼叫該迭代器的 __next__(),每次拿一個元素。
  3. 當沒有元素時,__next__() 會丟出 StopIteration 例外,for 捕捉到它就正常結束迴圈。

也就是說,下面這段「手寫的 while」與 for 在語意上是等價的:

nums = [10, 20, 30]

it = iter(nums)            # 等同 nums.__iter__()
while True:
    try:
        x = next(it)       # 等同 it.__next__()
    except StopIteration:
        break
    print(x)
# 輸出:
# 10
# 20
# 30

理解這層機制能解釋許多現象。例如:可迭代物(iterable)迭代器(iterator) 是兩個概念——串列是可迭代物(每次 iter() 都給你一個全新、從頭開始的迭代器),但迭代器本身只能走一遍,走完就枯竭:

gen = (n for n in range(3))
print(list(gen))           # 輸出:[0, 1, 2]
print(list(gen))           # 輸出:[]  ← 已經被走完,第二次是空的

你也可以讓自己的類別支援 for,只要實作 __iter__(與必要時的 __next__)即可。下面這個倒數計時器就是一個自製的可迭代物:

class CountDown:
    def __init__(self, start):
        self.start = start

    def __iter__(self):
        n = self.start
        while n > 0:
            yield n        # 用 yield 自動產生迭代器
            n -= 1

for x in CountDown(3):
    print(x, end=" ")
print()
# 輸出:3 2 1

這裡用了 yield:含有 yield 的函式是產生器函式(generator function),呼叫它會回傳一個產生器物件,而產生器本身已經實作好 __iter____next__,每次 next() 會從上次 yield 的位置繼續執行。這正是為什麼產生器能「邊算邊吐、節省記憶體」——它把狀態凍結在函式內部,需要時才往前推進一步。

掌握了迭代器協定,你會發現 for、推導式、產生器、zipenumeratemap 其實都站在同一套抽象之上。這套統一的介面,正是 Python「資料走訪」如此一致而優雅的根本原因。

AI 共讀助教正在陪你讀:Python 流程控制:讓程式替你做決定與重複做事
嗨!我是這篇文章的共讀助教,只根據〈Python 流程控制:讓程式替你做決定與重複做事〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。