2.24. 情境管理器

with 陳述式會先執行設定程式碼,接著執行主體,然後執行收尾程式碼 -- 並保證即使主體中途失敗,收尾程式碼仍會執行。提供設定與收尾的這一對方法稱為 情境管理器

其形式為:

with <expression> as <name>:
    <body>

該運算式會回傳一個情境管理器。Python 會呼叫它的 __enter__ 方法,並選擇性地將結果繫結到 <name>,接著執行主體,然後呼叫 __exit__ -- 無論主體是正常完成還是引發例外。

流程圖:先執行 __enter__,接著是主體; 主體要麼正常完成,要麼引發例外;無論哪種情況, 最後都會執行 __exit__。

__exit__ 會在區塊結束時執行,無論區塊內部發生什麼事。

2.24.1. 使用情境管理器

最典型的範例是開啟檔案:

with open("data.txt") as f:
    text = f.read()

# f is now closed, even if read() failed

open() 會回傳一個本身就是情境管理器的檔案物件;__enter__ 回傳該檔案,__exit__ 則將其關閉。with 區塊讓「完成後一律關閉檔案」成為預設行為,而非需要呼叫者自行記住的事。

2.24.1.1. 多個情境管理器

單一個 with 陳述式可以一次進入多個情境管理器,以逗號分隔:

with open("input.txt") as src, open("output.txt", "w") as dst:
    dst.write(src.read())

這等同於兩個巢狀的 with 區塊,但更扁平。各管理器由左至右進入,並以相反順序退出;若右側管理器的 __enter__ 引發例外,左側管理器的 __exit__ 仍會執行。

2.24.2. 撰寫情境管理器

任何具有 __enter____exit__ 方法的類別都可作為情境管理器使用:

class Section:
    def __init__(self, label):
        self.label = label

    def __enter__(self):
        print("---", self.label, "---")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("--- end", self.label, "---")
        return False

with Section("setup"):
    print("doing the work")

輸出:

--- setup ---
doing the work
--- end setup ---

__exit__ 會接收三個描述結束該區塊之例外的引數,若區塊正常完成,則為三個 None 值。回傳 False(或 None)會讓任何例外在收尾後繼續傳播;回傳 True 則會吞掉它。

對任何具有「開啟/關閉」或「取得/釋放」生命週期的資源都可使用情境管理器 -- 不限於檔案。這種模式讓清理工作與設定在兩者引入之處成對出現,因此在一個長函式中段遺漏 close 也不會導致資源外洩。