使用 with as
May 4, 2022經常地,在使用 try
、finally
嘗試關閉資源時,會發現程式撰寫的流程是類似的,就如〈使用 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 as
的 as
不一定需要。例如:
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_reader
中 yield
進行執行,結果就是最後關閉檔案。
由於這個 file_reader
沒有捕捉例外,若是因為例外而進入 __exit__
,執行了 file_reader
,在 finally
關閉檔案後,例外會往外傳播。
with as
語法是用來表示,銜接的資源實例是處於某個情境,自動關閉檔案的情境只是其中一種情況,有興趣的話,可以看看 contextlib
的 suppress
、closing
等,看看它們封裝了何種情境、該如何使用,有空時也可以研究一下它們的原始碼。