Home
Explore Uedu
Student Console
Register as Member/Login
(2) In future presentations of the research findings, in addition to the course project website and public presentations, your real name and personal information will not appear in this research report. If you are interested in the research results, we can provide you with an executive summary after the study is completed.
問卷中心
Teacher Console
Course Setup
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

0

Local AI

--

CISOSE26 本地 AI UG26
政治大學 AQI 18 29°C

AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

流程控制

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 流程控制:讓程式替你做決定與重複做事〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。