使用 with as

May 4, 2022

經常地,在使用 tryfinally 嘗試關閉資源時,會發現程式撰寫的流程是類似的,就如〈使用 else、finally〉示範的,在 try 中進行指定的動作,最後在 finally 中關閉檔案。

with as

為了應付之後類似的需求,你可以自定義一個 with_file 函式。例如:


def with_file(f, consume):
    try:
        consume(f)
    finally:
        f.close()

有了 with_file 函式,就可以運用這個 with_file 函式來改寫方才的範例:

import sys

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except FileNotFoundError:
        print('找不到檔案', arg)
    else:
        with_file(f, lambda f: print(arg, ' 有 ', len(f.readlines()), ' 行 '))

對於其他的需求,也可以重用這個 with_file 函式。例如:

import sys, logging

def print_each_line(file):
    try:
        # 檔案物件可以使用 for in
        #下一章會說明
        for line in file:
            print(line, end = '')
    except:
        logger = logging.getLogger(__name__)
        logger.exception('未處理的例外')

try:
    with_file(open(sys.argv[1], 'r'), print_each_line)
except IndexError:
    print('請提供檔案名稱')
    print('範例:')
    print('    python read.py your_file')
except FileNotFoundError:
    print('找不到檔案 {0}'.format(sys.argv[1]))

實際上,不用自行定義 with_file 這樣的函式,Python 提供了 with as 語法來解決這類需求。例如:

import sys

for arg in sys.argv[1:]:
    try:
        with open(arg, 'r') as f:
            print(arg, ' 有 ', len(f.readlines()), ' 行 ')
    except FileNotFoundError:
        print('找不到檔案', arg)

with 之後銜接的資源實例,可以透過 as 來指定給一個變數,with as 語法是用來表示,銜接的資源實例會處於某個情境,就檔案來說,這個情境是指 with as 區塊結束後,會將 with 銜接的檔案關閉。

如果需要同時使用 with 來管理多個資源,可以使用逗號「,」區隔。例如:

with open(file_name1, 'r') as f1, open(file_name2, 'r') as f2:
    print(file_name1, ' 有 ', len(f1.readlines()), ' 行 ')
    print(file_name2, ' 有 ', len(f2.readlines()), ' 行 ')

with asas 不一定需要。例如:

f = open(file_name, 'r')
with f:
    print(file_name, ' 有 ', len(f.readlines()), ' 行 ')

情境管理器

實際上,with as 不限使用於檔案,只要物件支援情境管理協定(Context Management Protocol),就可以使用 with as 語句。

支援情境管理協定的物件,必須實作 __enter____exit__ 兩個方法,這樣的物件稱為情境管理器(Context Manager)。

with 陳述句一開始執行,就會進行 __enter__ 方法,該方法傳回的物件,可以使用 as 指定給變數(如果有的話),接著就執行 with 區塊中的程式碼,以下是個簡單示範:

class Resource:
    def __init__(self, name):
        self.name = name

    def __enter__(self):
        print(self.name, ' __enter__')
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print(self.name, ' __exit__')
        return False

with Resource('res') as resource:
    print(resource.name)

如果 with 區塊執行完畢,會執行 __exit__ 方法,若是因例外而離開 with 區塊,會傳入三個引數,這三個引數是例外的類型、實例以及 traceback 物件,__exit__ 方法若傳回 False,例外會被重新引發,否則例外就停止傳播,通常 __exit__ 會傳回 False,以便在 with 之後還可以處理例外。

如果 with 區塊中沒有發生例外而執行完畢,也是執行 __exit__ 方法,此時 __exit__ 的三個參數都接收到 None。就上面的例子來說,會如下依序顯示:

res  __enter__
res
res  __exit__

open 函式傳回的檔案物件,本身就實作了 __enter____exit__,因此方才就能搭配 with as

@contextmanager

雖然可以直接實作 __enter____exit__ 方法,讓物件能支援 with as,不過將資源的設定與清除,分開在兩個方法中實作,顯得不夠直覺,可以使用 contextlib 模組的 @contextmanager 來實作,讓資源的設定與清除更為直覺。

方才談到,with as 語法是用來表示,銜接的資源實例是處於某個情境,就檔案來說,這個情境是指 with as 區塊結束後,會將 with 銜接的檔案關閉,來假裝一下 open 函式傳回的檔案物件,沒有 __enter____exit__,若使用 @contextmanager 來管理檔案關閉的話,該怎麼寫:

import sys
from contextlib import contextmanager

@contextmanager
def file_reader(filename):
    try:
        f = open(filename, 'r')
        yield f
    finally:
        f.close()

with file_reader(sys.argv[1]) as f:
    for line in f:
        print(line, end='')

現在呼叫 file_reader 後傳回的物件,會實現 __enter____exit__ 方法,with 呼叫該物件的 __enter__ 方法後,會執行 file_reader 定義的流程,yield 的物件,將會是 as 後指定的變數參考之物件,with as 區塊執行完畢或因為例外而離開區塊,會執行 __exit__ 方法,流程會回到 file_readeryield 進行執行,結果就是最後關閉檔案。

由於這個 file_reader 沒有捕捉例外,若是因為例外而進入 __exit__,執行了 file_reader,在 finally 關閉檔案後,例外會往外傳播。

with as 語法是用來表示,銜接的資源實例是處於某個情境,自動關閉檔案的情境只是其中一種情況,有興趣的話,可以看看 contextlibsuppressclosing 等,看看它們封裝了何種情境、該如何使用,有空時也可以研究一下它們的原始碼。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter