Day 11 檔案讀寫與例外處理
引言
到目前為止,我們的 Python 程式都在記憶體裡運作:變數、串列、字典這些資料,只要程式結束就消失了。如果想讓資料留下來,就得把結果寫到磁碟;想讀別人整理好的資料,也得從檔案讀進來。檔案讀寫是寫程式很常見的工作,不論是記錄使用者輸入、讀取設定檔、輸出報表,或是處理資料科學的 CSV,都脫離不了檔案操作。可以說,學會檔案讀寫之後,Python 才真正從「練習小工具」升級成「能處理真實資料的工具」。
不過,現實世界的檔案操作常常出問題:檔案可能不存在、權限不足、磁碟滿了,或讀到一半網路斷線。這時候如果沒有處理例外,程式就會直接當機,並把一段落落長的 Traceback 丟到使用者面前。今天除了學會基本的檔案讀寫,也會介紹 try / except 的機制,讓程式在出錯時能優雅地回應,而不是整支停下來。學完之後,你就能寫出對錯誤有耐受度的小工具,給同事或自己使用時也更安心。
讀取檔案:open() 與 with 敘述
Python 提供內建的 open() 函式來開啟檔案。最重要的兩個參數是檔案路徑與開啟模式:'r' 表示讀取(預設)、'w' 表示寫入、'a' 表示附加在檔尾。開啟後回傳的是一個檔案物件,可以呼叫 read()、readline()、readlines() 等方法讀取內容。讀取完成後要記得呼叫 close() 關閉檔案,否則可能會遺失部分資料、檔案被鎖住無法刪除,甚至在 Windows 上直接跳出「檔案已被另一個程式使用」的錯誤。
實務上更推薦用 with 敘述(context manager)。它會在區塊結束時自動關閉檔案,即使中間發生錯誤也會正確釋放資源。這種寫法不僅更安全,也讓程式碼更乾淨,省下手動呼叫 close() 的一行。
以下範例假設工作目錄下有一個 notes.txt(內容是幾行文字);如果手邊沒有這個檔案,可以先建立一個再跟著執行。
# 讀取整個檔案
with open("notes.txt", "r", encoding="utf-8") as f:
content = f.read()
print(content)
# 一次讀一行,逐行處理
with open("notes.txt", "r", encoding="utf-8") as f:
for line in f:
print(line.strip()) # 去除換行與空白
第一段把整個檔案讀成一個字串,適合內容不大的設定檔或小報告。第二段用 for 迴圈逐行走訪,處理大檔案時比較省記憶體。常見的 encoding="utf-8" 是為了正確讀寫中文,避免在不同作業系統間產生亂碼。Windows 預設的編碼是 cp950 或 cp1252,如果沒指定 encoding,可能會在中文環境讀到 Mojibake(亂碼)字元。
如果只想讀取前幾行或前幾個字元,也可以用 readline() 與 readlines():
with open("notes.txt", "r", encoding="utf-8") as f:
first_line = f.readline()
rest_lines = f.readlines() # 回傳剩餘行組成的串列
print(first_line)
print(rest_lines[:3]) # 只看前 3 行
readline() 一次讀一行(含換行符號),適合用在「讀到某個關鍵字就停止」這類流程控制。readlines() 則一次把所有剩餘行讀成串列,方便後續用索引或切片處理。如果要讀取的檔案很大(幾 GB),建議改用前面示範的逐行 for 迴圈,避免一次把所有內容塞進記憶體。
寫入檔案:覆寫與附加
開啟檔案時的模式決定了寫入行為。'w' 模式會建立新檔案或覆寫既有檔案,'a' 模式則會把內容附加到檔案尾端。要小心 'w' 會直接清空原本的內容,如果是想保留舊資料又想寫入新的,必須先讀出來合併再寫回去,或者改用 'a' 模式。寫入可以用 write()(寫入字串)或 writelines()(寫入一組字串,但 writelines 不會自動加換行)。
# 寫入新檔
lines = ["第一行\n", "第二行\n", "第三行\n"]
with open("output.txt", "w", encoding="utf-8") as f:
f.write("Hello, Python!\n")
f.writelines(lines)
# 附加內容到既有檔案
with open("output.txt", "a", encoding="utf-8") as f:
f.write("這是附加的一行\n")
執行後,output.txt 會包含四行文字。注意 write() 不會自動加上換行符號,需要自己寫 "\n",這是新手常見的坑。如果同時需要讀取與寫入,可以使用 'r+' 或 'w+' 等模式,但實務上多半是先讀後寫,分成兩段 with 區塊處理會更清楚,也比較不容易出錯。當需要寫入更複雜的結構(例如字典、串列)時,可以考慮用 json 模組,它能直接把 Python 物件序列化為文字並儲存,之後再讀回來還原,維護成本比純文字檔低很多。
例外處理基礎:try / except / else / finally
開啟檔案時最常見的錯誤就是 FileNotFoundError:指定的檔案不存在。這時候如果沒有處理,程式會立刻中斷並印出 Traceback,體驗很差。Python 用 try / except 來捕捉並處理例外(exception)。try 區塊裡放「可能出錯的程式碼」,except 區塊裡放「錯誤發生時要做的事」。
try:
with open("missing.txt", "r", encoding="utf-8") as f:
content = f.read()
print(content)
except FileNotFoundError:
print("找不到檔案,請確認路徑是否正確")
except PermissionError:
print("沒有讀取權限")
except Exception as e:
print(f"發生未預期的錯誤:{e}")
這個範例展示了幾個重點:except 後面可以指定例外型別,只處理我們關心的錯誤;多個 except 由上而下依序匹配,越具體的例外放越前面;最後一個 Exception 是所有例外的父類別,用來當作保險,攔截前面沒接住的錯誤。實際上,最好不要用「裸 except」也就是不指定任何錯誤別的寫法,因為連 KeyboardInterrupt 都被吃掉,會讓程式難以中斷。
完整的 try 結構還包含 else 與 finally:else 在沒有發生例外時執行,finally 則無論是否出錯都會跑,通常用來釋放資源或紀錄狀態。
try:
f = open("data.txt", "r", encoding="utf-8")
content = f.read()
except FileNotFoundError:
print("檔案不存在")
else:
print(f"成功讀取,內容長度:{len(content)}")
finally:
if "f" in locals() and not f.closed:
f.close()
print("讀檔流程結束")
把「可能成功才要做的事」放在 else,能讓主流程更清楚,避免例外處理區塊被太多「正常邏輯」塞住。finally 則很適合放紀錄檔(log)寫入或資源釋放,確保不管程式怎麼走,這些事情一定會被執行。實際專案中,用 with 敘述搭配 try / except 是最常見的組合。
自訂例外與 raise
有時候我們想主動觸發錯誤,而不是被動等 Python 丟例外。例如使用者輸入的年齡是負數,這在邏輯上不合理,應該告訴呼叫端這個狀況。這時候可以用 raise 主動拋出例外。raise 後面接例外實例(可以是內建的,也可以是自訂的),程式會從這個位置立刻跳離當前呼叫堆疊。
def set_age(age):
if not isinstance(age, int):
raise TypeError("年齡必須是整數")
if age < 0 or age > 150:
raise ValueError("年齡超出合理範圍 (0-150)")
print(f"設定年齡:{age}")
try:
set_age(-5)
except ValueError as e:
print(f"錯誤:{e}")
主動拋出例外比「回傳 -1 代表錯誤」這種用約定俗成的特殊值好得多,因為它強迫呼叫端必須處理,無法不小心忽略。實務上也常在函式入口做參數檢查,盡量把錯誤擋在離問題最近的地方,讓問題的根源更明確。
當專案變大,我們可以自訂例外類別,讓錯誤訊息更貼近業務情境。自訂例外只要繼承 Exception 或它的子類別即可。建議繼承最接近語意的內建例外(例如值錯誤就繼承 ValueError),這樣呼叫端既可以選擇針對特定自訂例外處理,也可以選擇針對父類別處理。
class InvalidAgeError(Exception):
"""年齡不合理的例外"""
pass
def register(name, age):
if age < 0:
raise InvalidAgeError(f"使用者 {name} 的年齡不可小於 0")
print(f"註冊 {name} ({age} 歲)")
try:
register("小明", -1)
except InvalidAgeError as e:
print(f"註冊失敗:{e}")
自訂例外的好處是呼叫端可以針對特定錯誤型別做處理,不會被一堆內建例外淹沒,閱讀程式碼時也能更快理解錯誤的商業意義。一個專案通常會把自訂例外集中在 exceptions.py 或 errors.py 模組,方便統一管理。
結語
今天學會了 Python 處理檔案與例外的基本功:用 open() 搭配 with 敘述來讀寫檔案,用 try / except / finally 來應對錯誤,再用 raise 與自訂例外把錯誤訊息講清楚。這幾個工具是寫任何真實程式都會用到的,特別是資料處理流程,常常得面對「檔案不存在」「格式怪怪」「資料缺漏」等狀況,把例外處理好,程式會更可靠。建議實際找一個過去寫過的小工具,把檔案讀寫與例外處理補進去,感受一下前後差異。
明天,我們會進入模組與套件的世界,看看怎麼把程式拆成可重複使用的模組、用 pip 安裝第三方套件,以及用 venv 為不同專案建立獨立的 Python 環境。這些基礎會讓後續安裝 NumPy、Pandas 與 PyTorch 時更順暢。
留言
張貼留言