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

UeduGPTs

--

Jupyters

4

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

AI 回覆桌面通知

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

聊天訊息通知

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

聲音通知

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

檔案與例外處理

Python 檔案與例外處理:讓程式記住東西,也接得住意外

從 open 與 with 上下文管理、讀寫文字與 CSV,到 try/except/finally 與 raise,最後深入上下文管理協定、例外鏈與 EAFP 風格。

把一份名單存成檔案:當程式需要記住東西

想像你寫了一個小程式,幫課堂統計每位同學的出席次數。程式跑完,畫面印出漂亮的結果——然後你關掉終端機,一切歸零。下次再跑,又得從頭輸入一遍。

電腦的記憶體(memory)是健忘的:程式一結束,變數裡的資料就煙消雲散。要讓資料「活過」這次執行,我們需要把它寫進檔案(file)。而一旦牽涉到檔案,麻煩也跟著來:檔案可能不存在、磁碟可能滿了、別的程式可能正佔用著它。這時就輪到 Python 的例外處理(exception handling)登場,讓你的程式遇到意外時不會直接崩潰,而是優雅地接住問題。

這篇文章帶你把這兩件事一次學會:怎麼正確地讀寫檔案,以及怎麼在事情出錯時還能保持冷靜。

檔案與例外處理概念示意圖

open 與檔案模式:先打開門,才能進去

讀寫檔案的第一步永遠是 open(),它回傳一個檔案物件(file object),後續所有讀寫都透過這個物件進行。

f = open("attendance.txt", "w", encoding="utf-8")
f.write("小明,5\n")
f.write("小華,3\n")
f.close()

open() 的第二個參數是模式(mode),決定你要對檔案做什麼。常見模式如下:

模式 意義 檔案不存在時 已有內容時
"r" 讀取(read) 拋出例外 保留
"w" 寫入(write) 建立新檔 整個清空
"a" 附加(append) 建立新檔 接在尾端
"x" 獨佔建立 建立新檔 拋出例外
"r+" 讀寫並存 拋出例外 保留

這裡有個初學者最常踩的雷:"w"毫不留情地清空原本的檔案內容。如果你只是想多加幾筆資料而不是重寫整份,請用 "a"

另外請務必養成習慣:永遠明確指定 encoding="utf-8"。Python 在不同作業系統上的預設編碼不一樣(Windows 常是 cp950,Linux 多為 utf-8),不寫清楚的話,存中文時很容易在別台電腦上變成亂碼。

with:別忘了關門,讓 Python 幫你關

上面的範例有個隱患:如果 f.write() 中途出錯,f.close() 就永遠不會被執行,檔案會一直開著佔用系統資源。手動 close() 既容易忘記,又難以在出錯時保證執行。

Python 提供了上下文管理器(context manager),搭配 with 陳述式,無論區塊內是正常結束還是中途出錯,檔案都會自動關閉

with open("attendance.txt", "w", encoding="utf-8") as f:
    f.write("小明,5\n")
    f.write("小華,3\n")
# 離開 with 區塊,檔案已自動關閉,不需要再呼叫 f.close()

with 是寫檔案的標準慣例,符合 PEP 8 精神。從今天起,請把「開檔一定用 with」變成肌肉記憶。反過來說,看到別人手動 open()close() 而沒有用 with,那通常是該被改進的反模式(anti-pattern)。

讀檔同樣如此。讀取整份內容有好幾種方式:

# 一次讀進整個字串
with open("attendance.txt", "r", encoding="utf-8") as f:
    content = f.read()
    print(content)

# 逐行讀取(推薦:記憶體友善,大檔案也不怕)
with open("attendance.txt", "r", encoding="utf-8") as f:
    for line in f:
        print(line.strip())  # strip() 去掉每行尾端的換行符號 \n

直接 for line in f 逐行迭代是處理大檔案的好習慣:它一次只把一行載入記憶體,而不是把整個檔案塞進來。對一個有上百萬行的紀錄檔,這個差別可能是程式能不能跑得動的關鍵。

讀寫 CSV:結構化資料的通用語

純文字檔適合存隨意內容,但當資料有「欄位」概念(像姓名、出席次數),CSV(Comma-Separated Values,逗號分隔值)格式更合適。它本質上就是用逗號分隔欄位的文字檔,Excel、試算表、資料庫都讀得懂。

你當然可以自己用 split(",") 硬切,但一旦欄位內容本身含有逗號或換行,手刻解析就會出錯。Python 標準函式庫的 csv 模組已經幫你處理好這些邊角情況,直接用就對了:

import csv

# 寫入 CSV
rows = [
    ["姓名", "出席次數"],
    ["小明", 5],
    ["小華", 3],
]
with open("attendance.csv", "w", encoding="utf-8", newline="") as f:
    writer = csv.writer(f)
    writer.writerows(rows)

注意寫 CSV 時多了一個 newline="" 參數——這是官方文件明確要求的,能避免在 Windows 上每行之間多出一個空白列。

讀 CSV 時,csv.DictReader 特別好用:它會把第一列當成欄位名稱,之後每一列都變成一個字典(dict),存取欄位時用名字而非索引,可讀性大增:

import csv

with open("attendance.csv", "r", encoding="utf-8", newline="") as f:
    reader = csv.DictReader(f)
    for row in reader:
        # row 是 {"姓名": "小明", "出席次數": "5"}
        print(f'{row["姓名"]} 出席了 {row["出席次數"]} 次')

# 輸出:
# 小明 出席了 5 次
# 小華 出席了 3 次

提醒一點:從 CSV 讀出來的值一律是字串(str)。上面的 "5" 是字串而不是整數,如果你要做加總計算,記得先 int(row["出席次數"]) 轉型。

例外處理:當事情不照劇本走

現在回到開頭的擔憂。如果使用者輸入了一個不存在的檔名,會發生什麼事?

with open("不存在的檔案.txt", "r", encoding="utf-8") as f:
    content = f.read()
# FileNotFoundError: [Errno 2] No such file or directory: '不存在的檔案.txt'

程式直接崩潰,後面的程式碼全都不執行了。這種執行期間發生的錯誤,Python 稱為例外(exception)。好消息是,我們可以用 try / except 把它「接住」:

try:
    with open("不存在的檔案.txt", "r", encoding="utf-8") as f:
        content = f.read()
    print(content)
except FileNotFoundError:
    print("找不到這個檔案,請確認檔名是否正確。")

# 輸出:
# 找不到這個檔案,請確認檔名是否正確。

try 區塊裡是「可能出事」的程式碼;一旦發生指定型別的例外,執行會立刻跳到對應的 except 區塊,程式就不會崩潰了。

你可以針對不同例外做不同處理,分開撰寫多個 except

try:
    with open("data.csv", "r", encoding="utf-8") as f:
        first = f.readline()
        count = int(first)
except FileNotFoundError:
    print("檔案不存在")
except ValueError:
    print("檔案第一行不是有效的數字")

try 還有兩個搭檔子句,合起來就是完整的四件套:

try:
    f = open("report.txt", "r", encoding="utf-8")
    data = f.read()
except FileNotFoundError:
    print("檔案不存在")           # 只有出錯才執行
else:
    print("讀取成功,長度為", len(data))  # 只有「沒出錯」才執行
finally:
    print("無論如何都會執行的收尾工作")     # 一定執行
  • except:抓到指定例外時執行。
  • elsetry 區塊完全沒出錯才執行,適合放「成功後才該做的事」。
  • finally無論有沒有出錯都會執行,常用來做清理收尾(關連線、釋放資源)。即使 try 裡有 returnfinally 仍會在離開前執行。

一個反模式:別寫「裸 except」

# ❌ 不要這樣做
try:
    risky_operation()
except:          # 連 Ctrl+C 中斷、系統結束訊號都會被吞掉
    pass

光禿禿的 except:(或 except Exception: 後什麼都不做)會把所有錯誤無聲無息地吞掉,讓你日後 debug 時找不到問題根源。請永遠抓明確的例外型別,並且至少留下有意義的訊息。

raise:主動拋出你自己的例外

例外不只能被動接住,你也能在發現資料不合理時主動拋出,用 raise。這在驗證輸入時非常實用:

def parse_attendance(count_str):
    count = int(count_str)
    if count < 0:
        raise ValueError(f"出席次數不能是負數:{count}")
    return count

try:
    parse_attendance("-3")
except ValueError as e:
    print("資料有誤:", e)

# 輸出:
# 資料有誤: 出席次數不能是負數:-3

as e 讓你拿到例外物件,可以印出它攜帶的訊息。讓函式在「明顯錯誤的輸入」上及早拋出例外,比讓壞資料默默流到程式深處才爆炸要好得多——這就是所謂的「fail fast(快速失敗)」原則。

動手寫一段:統計出席名單

把上面所有概念串起來,寫一個有頭有尾、能容錯的小程式。它會讀取一份 CSV、加總所有出席次數,並妥善處理檔案不存在或資料格式錯誤的情況。

import csv

def total_attendance(path):
    """讀取出席 CSV,回傳總出席次數。"""
    total = 0
    try:
        with open(path, "r", encoding="utf-8", newline="") as f:
            reader = csv.DictReader(f)
            for row in reader:
                count = int(row["出席次數"])   # 字串轉整數
                if count < 0:
                    raise ValueError(f"出現負數:{row['姓名']}")
                total += count
    except FileNotFoundError:
        print(f"找不到檔案:{path}")
        return None
    except ValueError as e:
        print(f"資料格式錯誤:{e}")
        return None
    else:
        return total
    finally:
        print("=== 統計流程結束 ===")

# 先準備一份測試資料
with open("attendance.csv", "w", encoding="utf-8", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["姓名", "出席次數"])
    writer.writerow(["小明", 5])
    writer.writerow(["小華", 3])
    writer.writerow(["小美", 4])

result = total_attendance("attendance.csv")
print("總出席次數:", result)

# 輸出:
# === 統計流程結束 ===
# 總出席次數: 12

試著把檔名改成不存在的路徑,或在 CSV 裡塞一個負數,看看程式如何接住錯誤而不崩潰——這就是穩健程式的樣子。

重點回顧:初學者最常踩的雷

  • "w" 模式會清空檔案:想保留舊內容只是「多寫幾行」,請改用 "a"。看到資料莫名消失,先檢查是不是用錯模式。
  • 忘記指定 encoding:不寫 encoding="utf-8" 在跨平台時很容易中文亂碼。養成每次都寫的習慣。
  • except: 吞掉一切:請抓明確的例外型別。無差別捕捉會讓 bug 藏得無影無蹤。
  • CSV 讀出來都是字串int(row["欄位"]) 別忘記轉型,否則 "5" + "3" 會得到 "53" 而不是 8
  • 忘了關檔:只要堅持用 with,這個問題就根本不會發生。不要再手動 open() / close()

深入探討(研究所視角)

上下文管理協定:with 背後到底發生了什麼

with 看起來像魔法,但它其實只是語法糖。任何物件只要實作了上下文管理協定(context manager protocol)——也就是 __enter____exit__ 兩個特殊方法——就能用在 with 裡。檔案物件之所以能搭配 with,正是因為它實作了這兩個方法。

我們可以自己手刻一個,看清楚它的運作:

class Timer:
    def __enter__(self):
        import time
        self.start = time.perf_counter()
        return self          # 回傳值會綁到 as 後面的變數

    def __exit__(self, exc_type, exc_value, traceback):
        import time
        elapsed = time.perf_counter() - self.start
        print(f"耗時 {elapsed:.4f} 秒")
        return False         # 回傳 False:不吞掉例外,讓它繼續往外傳

with Timer() as t:
    total = sum(range(1_000_000))

# 輸出(數值依機器而異):
# 耗時 0.0123 秒

關鍵細節在 __exit__ 的三個參數:當 with 區塊內發生例外時,Python 會把例外的型別、值、追溯資訊(traceback)傳進來;正常結束時則三個都是 None__exit__回傳值決定例外的命運——回傳真值(truthy)代表「我處理好了,吞掉這個例外」,回傳 FalseNone 則讓例外照常往外傳播。這也解釋了為什麼 with open(...) 即使區塊內爆炸,檔案依然會被關閉:關檔邏輯就寫在 __exit__ 裡,而它保證會被呼叫。標準函式庫的 contextlib.contextmanager 裝飾器更讓你能用一個帶 yield 的生成器函式來寫上下文管理器,省去寫整個類別的工夫。

例外鏈:別讓原始錯誤消失

當你在處理一個例外的過程中又拋出新例外,Python 會自動把兩者串接(chain)起來,保留完整的因果脈絡:

def load_config(path):
    try:
        with open(path, "r", encoding="utf-8") as f:
            return f.read()
    except FileNotFoundError as e:
        raise RuntimeError("設定載入失敗") from e   # 明確標示因果

raise ... from e 會在 traceback 印出「The above exception was the direct cause of the following exception」,讓除錯者同時看到「表層錯誤」與「底層真因」。如果你不寫 from e,Python 仍會隱式鏈接(顯示為「During handling of the above exception...」);但用 from 明確表達意圖是更專業的做法。若想刻意切斷鏈結,可用 from None

EAFP vs LBYL:兩種程式設計哲學

處理「可能出錯」的情況,有兩種根本不同的風格。

LBYL(Look Before You Leap,三思而後行)——先檢查條件再動作:

import os
if os.path.exists(path):          # 先檢查
    with open(path) as f:
        data = f.read()
else:
    data = None

EAFP(Easier to Ask Forgiveness than Permission,先做了再說,出錯再道歉)——直接做,用例外接住失敗:

try:
    with open(path) as f:
        data = f.read()
except FileNotFoundError:
    data = None

Python 社群慣用且推薦 EAFP,原因不只是風格偏好。LBYL 有個隱蔽的競態條件(race condition)問題:在你 os.path.exists() 檢查通過、到真正 open() 的這短短一瞬間,檔案可能被另一個行程刪除——這就是經典的 TOCTOU(Time-Of-Check to Time-Of-Use)漏洞。EAFP 直接嘗試操作,把檢查與動作合為一個原子步驟,從根本上避開了這個縫隙。此外,當「正常路徑」遠比「出錯路徑」常見時,EAFP 省去了每次都做檢查的成本——try 區塊在沒有例外時幾乎是零開銷($O(1)$ 且常數極小),只有真正拋出例外時才付出代價。

理解了這三件事——上下文管理協定如何保證資源釋放、例外鏈如何保存除錯線索、EAFP 如何兼顧安全與效能——你對 Python 例外與檔案處理的掌握就從「會用」進階到「懂為什麼」了。接下來,不妨把今天的出席統計程式擴充成能處理多個班級檔案的版本,親手體會這些機制在真實情境裡的價值。

AI 共讀助教正在陪你讀:Python 檔案與例外處理:讓程式記住東西,也接得住意外
嗨!我是這篇文章的共讀助教,只根據〈Python 檔案與例外處理:讓程式記住東西,也接得住意外〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。