什麼是上下文管理器,Python with as底層原理詳解

2020-07-16 10:05:01
在介紹 with as 語句時講到,該語句操作的物件必須是上下文管理器。那麼,到底什麼是上下文管理器呢?

簡單的理解,同時包含 __enter__() 和 __exit__() 方法的物件就是上下文管理器。也就是說,上下文管理器必須實現如下兩個方法:
  1. __enter__(self):進入上下文管理器自動呼叫的方法,該方法會在 with as 程式碼塊執行之前執行。如果 with 語句有 as子句,那麼該方法的返回值會被賦值給 as 子句後的變數;該方法可以返回多個值,因此在 as 子句後面也可以指定多個變數(多個變數必須由“()”括起來組成元組)。
  2. __exit__(self, exc_type, exc_value, exc_traceback):退出上下文管理器自動呼叫的方法。該方法會在 with as 程式碼塊執行之後執行。如果 with as 程式碼塊成功執行結束,程式自動呼叫該方法,呼叫該方法的三個引數都為 None:如果 with as 程式碼塊因為異常而中止,程式也自動呼叫該方法,使用 sys.exc_info 得到的異常資訊將作為呼叫該方法的引數。

當 with as 操作上下文管理器時,就會在執行語句體之前,先執行上下文管理器的 __enter__() 方法,然後再執行語句體,最後執行 __exit__() 方法。

構建上下文管理器,常見的有 2 種方式:基於類實現基於生成器實現

基於類的上下文管理器

通過上面的介紹不難發現,只要一個類實現了 __enter__() 和 __exit__() 這 2 個方法,程式就可以使用 with as 語句來管理它,通過 __exit__() 方法的引數,即可判斷出 with 程式碼塊執行時是否遇到了異常。其實,上面程式中的檔案物件也實現了這兩個方法,因此可以接受 with as 語句的管理。

下面我們自定義一個實現上下文管理協定的類,並嘗試用 with as 語句來管理它:
class FkResource:
    def __init__(self, tag):
        self.tag = tag
        print('構造器,初始化資源: %s' % tag)
    # 定義__enter__方法,with體之前的執行的方法
    def __enter__(self):
        print('[__enter__ %s]: ' % self.tag)
        # 該返回值將作為as子句中變數的值
        return 'fkit'  # 可以返回任意型別的值
    # 定義__exit__方法,with體之後的執行的方法
    def __exit__(self, exc_type, exc_value, exc_traceback):
        print('[__exit__ %s]: ' % self.tag)
        # exc_traceback為None,代表沒有異常
        if exc_traceback is None:
            print('沒有異常時關閉資源')
        else:
            print('遇到異常時關閉資源')
            return False   # 可以省略,預設返回None也被看做是False
with FkResource('孫悟空') as dr:
    print(dr)
    print('[with程式碼塊] 沒有異常')
print('------------------------------')
with FkResource('白骨精'):
    print('[with程式碼塊] 異常之前的程式碼')
    raise Exception
    print('[with程式碼塊] ~~~~~~~~異常之後的程式碼')
執行上面的程式,可以看到如下輸出結果:

構造器,初始化資源: 孫悟空
[__enter__ 孫悟空]:
fkit
[with程式碼塊] 沒有異常
[__exit__ 孫悟空]:
沒有異常時關閉資源
------------------------------
構造器,初始化資源: 白骨精
[__enter__ 白骨精]:
[with程式碼塊] 異常之前的程式碼
[__exit__ 白骨精]:
遇到異常時關閉資源
Traceback (most recent call last):
  File "C:UsersmengmaDesktop1.py", line 26, in <module>
    raise Exception
Exception

上面程式定義了一個 FkResource 類,並包含了 __enter__() 和 __exit__() 兩個方法,因此該類的物件可以被 with as 語句管理。

此外,程式中兩次使用 with as 語句管理 FkResource 物件。第一次程式碼塊沒有出現異常,第二次程式碼塊出現了異常。從上面的輸出結果來看,使用 with as 語句管理資源,無論程式碼塊是否有異常,程式總可以自動執行 __exit__() 方法。

注意,當出現異常時,如果 __exit__ 返回 False(預設不寫返回值時,即為 False),則會重新丟擲異常,讓 with as 之外的語句邏輯來處理異常;反之,如果返回 True,則忽略異常,不再對異常進行處理。

基於生成器的上下文管理器

除了基於類的上下文管理器,它還可以基於生成器實現。接下來先看一個例子。比如,我們可以使用裝飾器 contextlib.contextmanager,來定義自己所需的基於生成器的上下文管理器,用以支援 with as 語句:
from contextlib import contextmanager

@contextmanager
def file_manager(name, mode):
    try:
        f = open(name, mode)
        yield f
    finally:
        f.close()
       
with file_manager('a.txt', 'w') as f:
    f.write('hello world')
這段程式碼中,函數 file_manager() 就是一個生成器,當我們執行 with as 語句時,便會開啟檔案,並返回檔案物件 f;當 with 語句執行完後,finally 中的關閉檔案操作便會執行。另外可以看到,使用基於生成器的上下文管理器時,不再用定義 __enter__() 和 __exit__() 方法,但需要加上裝飾器 @contextmanager,這一點新手很容易疏忽。

需要強調的是,基於類的上下文管理器和基於生成器的上下文管理器,這兩者在功能上是一致的。只不過,基於類的上下文管理器更加靈活,適用於大型的系統開發,而基於生成器的上下文管理器更加方便、簡潔,適用於中小型程式。但是,無論使用哪一種,不用忘記在方法“__exit__()”或者是 finally 塊中釋放資源,這一點尤其重要。