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),只有 True 與 False 兩種。常用的比較運算子有 ==(相等)、!=(不相等)、<、<=、>、>=。
特別提醒:判斷相等用兩個等號 ==,單一等號 = 是「賦值」。把 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)」:在條件中,空字串 ""、空串列 []、0、None 都會被當作假,非空與非零則為真。所以判斷串列非空時,慣例是直接寫 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 ... else。else 區塊只在迴圈沒有被 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 實際做了這些事:
- 先呼叫
iter(obj),也就是obj.__iter__(),取得一個迭代器(iterator)。 - 不斷呼叫該迭代器的
__next__(),每次拿一個元素。 - 當沒有元素時,
__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、推導式、產生器、zip、enumerate、map 其實都站在同一套抽象之上。這套統一的介面,正是 Python「資料走訪」如此一致而優雅的根本原因。