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)
進階:使用 ExitStack 動態管理資源
如果你要同時管理「數量未知」的多個資源,或是想根據條件動態決定要不要啟動某個 Context Manager,contextlib.ExitStack 是一個非常強大的工具。
動態開啟多個檔案:enter_context()
假設你有一個檔案名稱列表,且數量不固定,傳統的 with 語法很難處理。這時可以使用 ExitStack.enter_context():
from contextlib import ExitStack
filenames = ["file1.txt", "file2.txt", "file3.txt"]
files = []
with ExitStack() as stack:
for fname in filenames:
# 動態將開啟的檔案加入 stack 中
f = stack.enter_context(open(fname, "w"))
files.append(f)
# 這裡可以一次操作所有被開啟的檔案
for f in files:
f.write("Hello\n")
# 離開 with 區塊後,stack 會自動把所有檔案關閉
ExitStack 就像一個堆疊 (Stack),它會記住所有透過 enter_context() 進入的 Context Manager,並在離開 with 區塊時,以後進先出 (LIFO) 的順序依次呼叫它們的清理動作(例如 close())。
註冊清理動作:callback()
除了管理 Context Manager,你也可以用 callback() 隨時註冊一個普通的函式,讓它在離開 with 區塊時自動執行:
from contextlib import ExitStack
def my_cleanup(name):
print(f"執行清理工作: {name}")
with ExitStack() as stack:
# 註冊一個普通函式作為清理動作
stack.callback(my_cleanup, "清空快取")
stack.callback(my_cleanup, "斷開連線")
print("正在執行主要任務...")
# 輸出:
# 正在執行主要任務...
# 執行清理工作: 斷開連線
# 執行清理工作: 清空快取
可以看到,callback() 也是以後進先出的順序執行的。
取消清理動作:pop_all()
如果你在設定資源的過程中發生錯誤,你希望立刻關閉已經開啟的資源;但如果一切順利,你想把這些資源打包回傳給外部使用(不要立刻關閉)。這時可以使用 pop_all():
from contextlib import ExitStack
def setup_multiple_resources():
with ExitStack() as stack:
f1 = stack.enter_context(open("data1.txt", "w"))
f2 = stack.enter_context(open("data2.txt", "w"))
# 假設如果一切正常,我們不想在這裡關閉檔案,
# 而是要把 stack 轉交給呼叫者去處理。
# pop_all() 會回傳一個包含了所有目前清理動作的新 ExitStack,
# 並且清空當前 stack 中的紀錄。
return stack.pop_all(), f1, f2
# 如果在中途發生錯誤,不會執行到 pop_all(),
# stack 還是會負責把已經開啟的檔案關閉。
# 外部接手管理
stack, f1, f2 = setup_multiple_resources()
with stack:
f1.write("data 1")
f2.write("data 2")
# 外部離開 with 時才會關閉 f1 和 f2
ExitStack 是 Python 處理複雜資源管理時的進階武器,特別適合用在撰寫框架或底層函式庫。