Python with用法:自動關閉檔案

2020-07-16 10:04:44
在前面的程式中,我們都採用了程式主動關閉檔案的方式。實際上,Python 提供了 with 語句來管理資源關閉。比如可以把開啟的檔案放在 with 語句中,這樣 with 語句就會幫我們自動關閉檔案。

with 語句的語法格式如下:

with context expression [as target(s)]:
    with 程式碼塊

在上面的語法格式中,context_expression 用於建立可自動關閉的資源。

例如,程式使用 with 語句來讀取檔案:
import codecs
# 使用with語句開啟檔案,該語句會負責關閉檔案
with codecs.open("readlines_test.py", 'r', 'utf-8', buffering=True) as f:
    for line in f:
        print(line, end='')
程式也可以使用 with 語句來處理通過 fileinput.input 合併的多個檔案,例如如下程式:
import fileinput
# 使用with語句開啟檔案,該語句會負責關閉檔案
with fileinput.input(files=('test.txt', 'info.txt')) as f:
    for line in f:
        print(line, end='')
上面兩個程式都使用了 with 語句來管理資源,因此它們都不需要顯式關閉檔案。

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

通過上面的介紹不難發現,只要一個類實現了 __enter__() 和 __exit__(exc_type, exc_value, exc_traceback) 方法,程式就可以使用 with 語句來管理它;通過 __exit__() 方法的引數,即可判斷出 with 程式碼塊執行時是否遇到了異常。

換而言之,上面程式所用的檔案物件、FileInput 物件,其實都實現了這兩個方法,因此它們都可以接受 with 語句的管理。

下面我們自定義一個實現上下文管理協定的類,並使用 with 語句來管理它:
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程式碼塊] ~~~~~~~~異常之後的程式碼')
上面程式定義了一個 FkResource 類,該類定義了 __enter__() 和 __exit__() 兩個方法,因此該類的物件可以被 with 語句管理:
  • 程式在執行 with 程式碼塊之前,會執行 __enter__() 方法,並將該方法的返回值賦值給 as 子句後的變數。
  • 程式在執行 with 程式碼塊之後,會執行 __exit__() 方法,可以根據該方法的引數來判斷 with 程式碼塊是否有異常。

程式兩次使用 with 語句管理 FkResource 物件。第一次,with 程式碼塊沒有出現異常。第二次,with 程式碼塊出現了異常。大家可以看到,使用 with 語句兩次對 FkResource 的管理略有差異(主要是在 __exit()__ 方法中略有差異)。

執行上面的程式,可以看到如下輸出結果:

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

從上面的輸出結果來看,使用 with 語句管理資源,程式總可以在進入 with 程式碼塊之前自動執行 __enter__() 方法,無論 with 程式碼塊是否有異常,這個部分都是一樣的,而且 __enter__() 方法的返回值被賦值給了 as 子句後的變數,如上面的 ① 號輸出資訊所示。

對於 with 程式碼塊有異常和無異常這兩種情況,此時主要通過 exit() 方法的引數進行判斷,程式可針對 with 程式碼塊是否有異常分別進行處理,如程式中程式碼所示。