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 檔案與例外處理:讓程式記住東西,也接得住意外

從 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 檔案與例外處理:讓程式記住東西,也接得住意外〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。