Python file exception with context manager

[Python] 檔案處理與例外

寫真實程式遲早要碰兩件事:讀寫檔案、處理會出錯的操作。這篇把 open / with / pathlib,跟 try / except / else / finally / 自訂例外 / 自訂 context manager 串在一起講——它們本就是同一條設計線:用 context manager 確保資源在出錯時也會釋放。

open:讀寫檔案

f = open('a.txt', 'r', encoding='utf-8')
content = f.read()
f.close()

mode 參數:

mode意義
r讀(預設)
w寫,會清空原內容
a追加
x寫(檔案存在會錯)
b二進位(搭配其他 mode,例如 rb
+讀寫(搭配,例如 r+

encoding 強烈建議顯式指定,否則跨平台會踩雷(Windows 預設不是 UTF-8)。

with:上下文管理器(必用)

不要手動 close(),用 with 自動關檔,例外發生時也會關:

with open('a.txt', 'r', encoding='utf-8') as f:
    content = f.read()
# with 區塊結束自動 close

多檔同時開:

with open('a.txt') as fa, open('b.txt', 'w') as fb:
    fb.write(fa.read())

讀檔的幾種方式

with open('a.txt', encoding='utf-8') as f:
    # 1. 一次讀全部
    content = f.read()

    # 2. 一行一行讀(記憶體友善,大檔首選)
    for line in f:
        print(line.rstrip())   # rstrip 去尾端換行

    # 3. 讀成 list(小檔)
    lines = f.readlines()

    # 4. 讀指定 byte 數
    head = f.read(100)

寫檔

with open('out.txt', 'w', encoding='utf-8') as f:
    f.write('hello\n')
    f.write('world\n')

# 或一次寫多行
with open('out.txt', 'w', encoding='utf-8') as f:
    f.writelines(['a\n', 'b\n', 'c\n'])

二進位

with open('img.png', 'rb') as f:
    data = f.read()

with open('out.bin', 'wb') as f:
    f.write(b'\x00\x01\x02')

pathlib:取代字串拼路徑

os.path 老舊。新程式碼一律用 pathlib.Path

from pathlib import Path

p = Path('data') / 'users' / 'jeremy.json'
print(p)                # data/users/jeremy.json(會自動處理 OS 分隔符)

p.exists()
p.is_file()
p.is_dir()
p.parent                # Path('data/users')
p.name                  # 'jeremy.json'
p.stem                  # 'jeremy'
p.suffix                # '.json'

# 讀寫一行搞定
text = p.read_text(encoding='utf-8')
p.write_text('hello', encoding='utf-8')

data = p.read_bytes()
p.write_bytes(b'\x00')

# 列舉
for f in Path('data').iterdir():
    print(f)

for f in Path('data').rglob('*.json'):     # 遞迴
    print(f)

# 建立資料夾
Path('logs/2026').mkdir(parents=True, exist_ok=True)

例外:try / except / else / finally

try:
    n = int('abc')
except ValueError as e:
    print(f'parse failed: {e}')
except (KeyError, IndexError) as e:        # 同時抓多種
    print(f'lookup failed: {e}')
else:
    print('no exception')                  # try 區塊沒丟例外才執行
finally:
    print('cleanup')                       # 一定會執行

各分支用途:

分支何時跑
try主邏輯
except捕到對應例外才跑
elsetry 沒丟例外才跑(可選)
finally一定會跑,包含釋放資源(可選)

raise:丟例外

def divide(a, b):
    if b == 0:
        raise ValueError('cannot divide by zero')
    return a / b

抓到後重新丟:

try:
    do_work()
except SomeError as e:
    log(e)
    raise           # 不帶任何東西,重丟原例外

包裝成更高階的例外(保留原因):

try:
    do_work()
except IOError as e:
    raise RuntimeError('cannot load config') from e

自訂例外

繼承 Exception 即可:

class APIError(Exception):
    pass

class NotFoundError(APIError):
    pass

class RateLimitError(APIError):
    def __init__(self, retry_after: int):
        super().__init__(f'rate limited, retry after {retry_after}s')
        self.retry_after = retry_after

try:
    raise RateLimitError(30)
except RateLimitError as e:
    print(e.retry_after)
except APIError as e:        # 父類能接子類的例外
    print('other API error')

慣例:library 都定義一個自家 base error,使用者可以 except YourLib.Error 一網打盡。

不要這樣寫

# ❌ 抓 Exception 會吞掉所有錯(包含 KeyboardInterrupt 沒到那麼上面,但仍掩蓋很多 bug)
try:
    do_work()
except:
    pass

# ❌ 太寬,至少要記錄
try:
    do_work()
except Exception:
    pass

正確:

  • 只抓你預期會發生且有處理對策的例外
  • 沒對策就讓它往上拋
  • 真要記錄一律 logging.exception(...) 帶上 stack

自訂上下文管理器

實作 __enter__ / __exit__

import time

class Timer:
    def __enter__(self):
        self.t = time.perf_counter()
        return self
    def __exit__(self, exc_type, exc_value, tb):
        elapsed = time.perf_counter() - self.t
        print(f'elapsed {elapsed:.3f}s')
        # 回傳 True 表示「壓下例外」(少用,預設別動)

with Timer():
    do_heavy_work()

三個參數:exc_type 是例外類別、exc_value 是例外 instance、tb 是 traceback 物件。正常離開 with 時三個都是 None。回傳 True 表示「例外我收下了」,會吞掉該例外;回傳 False 或不回傳則讓例外繼續往上傳。

或用 contextlib

from contextlib import contextmanager

@contextmanager
def timer():
    import time
    t = time.perf_counter()
    try:
        yield
    finally:
        print(f'elapsed {time.perf_counter() - t:.3f}s')

with timer():
    do_heavy_work()

yield 之前對應 __enter__,之後對應 __exit__try / finally 確保例外時也會跑釋放邏輯。

執行流程:進入 with 時跑到 yield 停住,yield 的值綁給 as 變數;區塊結束(不論正常或拋例外)後,從 yield 處繼續往下跑完 try/finally。若區塊內拋了 exception,它會在 yield 處重新 raise——想壓下例外,就用 try/except 包住 yield

常用內建例外

例外觸發
ValueError型別對但值不合(int('abc')
TypeError型別錯('a' + 1
KeyErrordict 找不到 key
IndexErrorlist 越界
AttributeError物件沒這屬性
FileNotFoundError檔案不存在
PermissionError權限不足
ZeroDivisionError除以 0
StopIterationiterator 結束
KeyboardInterruptCtrl+C
SystemExitsys.exit() 觸發

KeyboardInterruptSystemExit 繼承自 BaseException 而非 Exception,所以 except Exception 不會抓到它們(這是好事——Ctrl+C 不該被你的錯誤處理吞掉)。

下一篇逛標準庫——jsondatetimecollectionsitertoolsfunctools 這些每個 Python 程式幾乎都會 import 的東西。

Latest Updates

  • 2026.06.11 Content updated