小票列印機寫到一半卡紙,檔案卻一直開著。with 塊承諾即使主體出錯也會收尾。上下文管理器到底保證了什麼?
with 程式碼區塊會完成準備工作、執行主體,並保證即使主體拋出例外也會清理。檔案是最常見的例子,但同一套協議也適用於鎖、臨時狀態、資料庫事務和測試夾具。 你還會抑制一個預期例外、在失敗時清理,並管理數量不定的資源。
讓 with 負責關閉檔案
with 語句會自動呼叫檔案的上下文管理器方法。程式碼區塊結束時檔案會關閉,即使有例外向外傳播也一樣。不需要在每條路徑上單獨呼叫 close。
from pathlib import Path
path = Path("pypractice-context.txt")
path.write_text("hello\n", encoding="utf-8")
with path.open(encoding="utf-8") as handle:
print(handle.read().strip())
path.unlink()自動清理
理解 enter 和 exit
上下文管理器實現 __enter__ 來準備資源,實現 __exit__ 來接收例外資訊並清理。with 語句會在主體前後呼叫它們。__exit__ 回傳假值時,例外會繼續向外傳播。
class Guard:
def __enter__(self):
print("enter")
return self
def __exit__(self, exc_type, exc, tb):
print("exit")
with Guard():
print("inside")上下文管理器協議
用生成器快速構造
contextlib.contextmanager 可以把生成器變成上下文管理器。yield 之前是準備,yield 之後是清理。yield 出來的值會綁定到 as 後面的變數。
from contextlib import contextmanager
@contextmanager
def label(name):
print("start", name)
yield name
print("end", name)
with label("demo") as value:
print("body", value)緊湊的自定義管理器
__exit__ 可以抑制一個例外
從 __exit__ 回傳 True 會抑制例外。只對範圍很窄、確實預期的失敗使用這種能力,例如可選檔案不存在。對所有例外都回傳 True 會隱藏 bug;contextlib.suppress 能用更少的程式碼表達同樣意圖。
class IgnoreMissing:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
if exc_type is FileNotFoundError:
print("ignored missing file")
return True
return False
with IgnoreMissing():
raise FileNotFoundError("demo")
print("after")只抑制預期型別
主體失敗時也要清理
contextmanager 函式應把清理放在 yield 外面的 finally 裡。正常退出和例外退出都會執行 yield 之後的程式碼,但前提是控制流回到生成器。把例外捕獲放在 with 外面,能讓清理和錯誤處理彼此分開。
from contextlib import contextmanager
@contextmanager
def session():
print("open")
try:
yield "ready"
finally:
print("close")
try:
with session() as state:
print(state)
raise RuntimeError("boom")
except RuntimeError as error:
print("caught", error)yield 放進 try,清理放進 finally
ExitStack 管理動態資源
資源數量要到執行時才知道時,ExitStack 可以逐個登記。程式碼區塊退出時,它會按相反順序關閉。這樣不必寫出層數不定的 with 巢狀。
from contextlib import ExitStack
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as tmp:
root = Path(tmp)
with ExitStack() as stack:
handles = []
for name in ["a.txt", "b.txt"]:
handle = stack.enter_context((root / name).open("w", encoding="utf-8"))
handles.append(handle)
handle.write(name)
print(len(handles), "open")
print("all closed")建立資源時逐個登記
本課要點
- with 在程式碼區塊退出時保證清理。
- 上下文管理器實現 __enter__ 和 __exit__。
- __exit__ 接收例外資訊,也可以抑制例外。
- contextlib.contextmanager 用一個 yield 把生成器變成管理器。
- 窄範圍抑制例外;finally 負責清理;ExitStack 管理動態資源。
常見錯誤
- 打開檔案卻不用 with,並且在每條路徑上都忘記關閉。
- 以為只有沒有例外時才會執行 __exit__。
- 忘記 contextmanager 中 yield 之後的程式碼是在清理階段執行。
- 對所有例外都從 __exit__ 回傳 True,把真正的 bug 藏起來。
用 with 打開一個小檔案,讀取內容,並在程式碼區塊結束後刪除它。
動手試試
改改變數、先預測輸出,再執行程式碼。第一次點擊「執行程式碼」時會下載 Python 執行時,可能需要幾秒鐘。
練習本課
12 道題,作答後立即看到解釋。