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:

  1. __enter__(self):進入 with 區塊時執行。回傳的值會被賦予給 as 後面的變數。
  2. __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")

重點:

  1. yield 之前的程式碼是「進入準備」。
  2. yield 之後的程式碼是「離開清理」。
  3. 必須用 try...finally 包果 yield,確保清理程式碼一定會執行。

常見應用場景

除了檔案操作,Context Manager 還常用於:

  1. 鎖 (Locks):在多執行緒中自動上鎖與解鎖。

    import threading
    lock = threading.Lock()
    with lock:
        # 自動 acquire()
        # 執行緒安全的操作
        pass # 自動 release()
    
  2. 資料庫交易 (Database Transaction):確保交易正確 Commit 或 Rollback。

    # 偽代碼範例
    # with db.transaction():
    #     user = User.create(...)
    #     Profile.create(...)
    # 成功則 commit,失敗則 rollback
    
  3. 暫時更改環境設定:例如暫時將浮點數精度提高,做完計算後恢復。

巢狀 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)