第 28 課 · 進階 · 20 分鐘

Python 上下文管理器

用 with 程式碼區塊保證準備和清理一定發生。

上下文管理器with__enter____exit__

小票列印機寫到一半卡紙,檔案卻一直開著。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 執行時,可能需要幾秒鐘。

瀏覽器內 Python本機執行 · 無需帳號
首次執行會從 CDN 下載 Pyodide。修改後點擊「執行程式碼」。

練習本課

12 道題,作答後立即看到解釋。

查看 12 道題答案開始練習
← 裝飾器dataclasses →