Python Context Manager (上下文管理器) 與 with 語法
在 Python 中,管理資源(如開啟檔案、網路連線、資料庫連線或執行緒鎖)時,我們必須確保在使用完畢後正確地釋放這些資源。如果忘記釋放,可能會導致記憶體洩漏或檔案鎖死等問題。
with 語法(上下文管理器 Context Manager)就是為了解決這個問題而生的。它能確保進入代碼區塊前做準備工作,離開區塊後做清理工作,即使發生例外錯誤 (Exception) 也能確保執行清理。
基礎語法:with open()
最常見的例子就是開啟檔案。
傳統寫法 (不推薦)
如果不使用 with,你需要手動呼叫 close(),而且如果中間發生錯誤,close() 可能永遠不會被執行:
f = open("hello.txt", "w")
try:
f.write("Hello World")
finally:
# 確保發生錯誤時也能關閉檔案
f.close()
使用 with (推薦)
使用 with 語法,Python 會自動幫你呼叫 close(),程式碼更簡潔也更安全:
# 離開縮排區塊後,檔案會已自動關閉
with open("hello.txt", "w") as f:
f.write("Hello World")
運作原理:Context Management Protocol
with 語法的背後是透過 Context Management Protocol 運作的。任何實作了以下兩個魔法方法的物件,都可以作為 Context Manager:
__enter__(self):進入with區塊時執行。回傳的值會被賦予給as後面的變數。__exit__(self, exc_type, exc_value, traceback):離開with區塊時執行(無論是否發生例外)。
自定義一個 Context Manager (Class 寫法)
讓我們寫一個自定義的 Timer,用來計算程式執行時間:
import time
class Timer:
def __enter__(self):
self.start = time.time()
return self # 這個值會傳給 as 後面的變數
def __exit__(self, exc_type, exc_val, exc_tb):
self.end = time.time()
self.interval = self.end - self.start
print(f"程式執行時間: {self.interval} 秒")
# 回傳 False (預設) 表示若有例外發生,不攔截它,讓它繼續往外丟
# 使用我們自定義的 Timer
with Timer() as t:
for i in range(1000000):
pass
# 輸出:程式執行時間: 0.05 秒
__exit__ 的例外處理
__exit__ 接收的三個參數與例外有關。如果區塊內發生錯誤:
- 如果
__exit__回傳True:表示例外已被處理,不會向外丟出。 - 如果
__exit__回傳False(或 None):表示例外會繼續向外丟出 (Bubble up)。
這常用於製作各種「忽略錯誤」的管理器。
使用 @contextmanager 裝飾器 (Generator 寫法)
寫一個 Class 有點麻煩?Python 的 contextlib 模組提供了一個裝飾器,讓你用 Generator 函數就能快速建立 Context Manager。
這通常是更現代、更簡潔的寫法。
from contextlib import contextmanager
@contextmanager
def my_file_opener(filename, mode):
# __enter__ 部分
print("準備開啟檔案...")
f = open(filename, mode)
try:
yield f # yield 出去的值會給 as 變數
finally:
# __exit__ 部分
print("正在關閉檔案...")
f.close()
# 使用方式完全一樣
with my_file_opener("test.txt", "w") as f:
f.write("Hello via generator")
重點:
yield之前的程式碼是「進入準備」。yield之後的程式碼是「離開清理」。- 必須用
try...finally包果yield,確保清理程式碼一定會執行。
常見應用場景
除了檔案操作,Context Manager 還常用於:
鎖 (Locks):在多執行緒中自動上鎖與解鎖。
import threading lock = threading.Lock() with lock: # 自動 acquire() # 執行緒安全的操作 pass # 自動 release()資料庫交易 (Database Transaction):確保交易正確 Commit 或 Rollback。
# 偽代碼範例 # with db.transaction(): # user = User.create(...) # Profile.create(...) # 成功則 commit,失敗則 rollback暫時更改環境設定:例如暫時將浮點數精度提高,做完計算後恢復。
巢狀 with (Nested with)
如果你需要同時開啟多個資源,可以寫在一起:
with open("input.txt") as f_in, open("output.txt", "w") as f_out:
data = f_in.read()
f_out.write(data)