Context Management in Python¶
在執行程式的時候通常會需要存取資源,一般來說資源的來源可能是檔案、遠端連線、或是某種 Socket。當程式在調用資源的時候基本上包含兩個動作:
請求資源使用權 (以檔案來說就是讀或寫之類的)、以及
釋放資源使用權。
本篇我們將整理在 Python 中面對資源存取問題時,透過 with
的常見作法、其物件意涵、以及內建套件 contextlib
的一些使用時機。
單一檔案存取¶
假設我們有一個檔案 input.txt
:
Hello world! I am a file.
This is a new line.
透過 Python 我們可以透過以下程式碼實現讀檔並輸出到終端機上:
1 2 3 4 5 6 7 8 | # access `input.txt` file with reading permission obj = open('input.txt', 'r') # read content and print to stdout print(obj.read()) # release file resource obj.close() |
而透過 with 關鍵字可以幫助程式碼在這個過程中更簡潔:
1 2 | with open('input.txt', 'r') as f: print(f.read()) |
到目前為止是大家在學習 Python 的時候幾乎會碰觸到的課題,接下來我們將討論 with
在 Python 當中的物件意涵。
With 關鍵字在 Python 中的物件意涵¶
我們同樣盯著上面的範例程式碼看,實際上他相當於以下語法結構:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import sys # with open('input.txt', 'r') as f: obj = open('input.txt', 'r') f = obj.__enter__() exc = True # a flag to determine if process trap into the except block try: # inside the with block print(f.read()) except: # go to here if there is an exception happened inside the block # set flag as False to avoid double-free exc = False # collect exception information exc_type, exc_val, exc_tb = sys.exit_info() if not obj.__exit__(exc_type, exc_val, exc_tb): raise finally: if exc: # in this way there is no exception exc_type = exc_val = exc_tb = None obj.__exit__(exc_type, exc_val, exc_tb) |
在這段程式碼中,我們會發現:
在進入
with
區塊前,Python 會初始化obj
物件並觸發__enter__
方法以存取資源,並且將其回傳值賦予到f
;而當程序離開
with
區塊時,Python 會觸發obj
物件的__exit__
方法以釋放資源。
Note
在檔案的例子中,obj.__enter__
的回傳值實際上就是物件 obj
本身,而
obj.__exit__
方法就相當於呼叫 obj.close
方法。
事情似乎變得複雜起來,但理解這個機制後,我們可以結合 Python 本身的 duck typing 特性 去做更彈性的編排。舉來說,我們可以自定義一種類別,使得在檔案資源存取的時候結合計數器功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | class FileResource(object): # counting times of reading and writing write_count = 0 read_count = 0 def __init__(self, filename, mode): self.filename = filename self.mode = mode self.file = None # will be triggered when process enter the with block def __enter__(self): # handling counter if 'w' in self.mode: FileResource.write_count += 1 if 'r' in self.mode: FileResource.read_count += 1 # instantiate file object and return itself self.file = open(self.filename, self.mode) return self.file # will be triggered when process leave the with block def __exit__(self, exc_type, exc_val, exc_tb): # when exception raised inside the with block, `exc_type` would be the # type of the exception, `exc_val` would be the object of such type, # and `exc_tb` would be the object of TracebackType. # # return True if you want to surpress this exception return self.file.__exit__(exc_type, exc_val, exc_tb) if __name__ == '__main__': with FileResource('input.txt', 'w') as f: f.write('Hello world! I am a file.\nThis is a new line.') with FileResource('input.txt', 'r') as f: print('first time reading ...') with FileResource('input.txt', 'r') as f: print('second time reading ...') print('instantiate but not actually read ...') fr = FileResource('input.txt', 'r') print(FileResource.read_count, FileResource.write_count) |
在執行結果中,我們會發現計數器有成功的被運作,且當檔案沒有真正開啟時不會列入計數。
first time reading ...
second time reading ...
instantiate but not actually read ...
2 1
此時我們已經具備設計自定義資源管理器的基礎技能了,現在我們要藉由 Python 強大內建套件來實現更靈活的操作。
使用 contextlib¶
這個強大內建套件就是 contextlib,實務上他可以幫助 Python 在處理資源管理的時候可以更加優雅。
我們以 FileResource
為例,在 contextlib 的加持下它可以被簡化成以下形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | import contextlib write_count = 0 read_count = 0 @contextlib.contextmanager def FileResource(filename, mode): if 'w' in mode: global write_count write_count += 1 if 'r' in mode: global read_count read_count += 1 obj = open(filename, mode) try: yield obj except: # you can handle exception here raise finally: obj.close() if __name__ == '__main__': with FileResource('input.txt', 'w') as f: f.write('Hello world! I am a file.\nThis is a new line.') with FileResource('input.txt', 'r') as f: print('first time reading ...') with FileResource('input.txt', 'r') as f: print('second time reading ...') print('instantiate but not actually read ...') fr = FileResource('input.txt', 'r') print(FileResource.read_count, FileResource.write_count) |
此時會發現說我們不用自行宣告類別就可以實現同樣的功能,且不只是更短的行數,程式碼的邏輯也變得更清晰。
以下我們透過一些案例來介紹 contextlib
的一些使用方法、以及額外功能。
Case Study #1: 包裝函數¶
當有些語法比較彆扭的時候可以包裝起來讓程式更美觀
1 2 3 4 5 6 7 8 9 10 11 12 | import contextlib import asyncssh def get_host_info_by_config(config): ... @contextlib.contextmanger def open_connect_by_config(config): hostname, username = get_host_info_by_config(config) with asyncssh.connect(hostname, username=username) as conn: yield conn |
Case Study #2: 轉導 stdout 到檔案流¶
將在 stdout 接到的字串流當作資源,轉導引到其他檔案流 (如空裝置或標準錯誤流)。
1 2 3 4 5 6 7 8 9 10 | import contextlib import os import sys with contextlib.redirect_stdout(None): print('Do not show anything') with contextlib.redirect_stdout(sys.stderr): print('I would be flushed at any time!') |
Warning
Python 的 redirect_stdout
並無法捕捉所有 stdout 接收到的字串流,就算是 sys.stdout
也有同樣的問題,若要捕捉全部資料,需要去呼叫到底層的 file descriptor. 若有興趣的讀者請參考 Eli Bendersky 的文章: Redirecting all kinds of stdout in Python.
Case Study #3: 複數資源管理 ExitStack¶
複數資源的調用過程相當於 stack 的操作過程
contextlib 已經實作好了,不要重造輪子
1 2 3 4 5 6 7 | import contextlib filenames = ['input1.txt', 'input2.txt', 'input3.txt'] with contextlib.ExitStack() as stack: files = [stack.enter_context(open(fname)) for fname in filenames] # do something ... |
資源管理的深層應用¶
在上面的案例中的資源都是十分具體的,比如說一個檔案或是一個流,而透過上述程式碼所形成的結構我們都可以稱作資源管理,意思是說「資源」也可以是一個抽象概念,比如說一個程式執行的狀態。
Note
或者直接將這個結構理解成:對內部區塊程式碼的前處理及後處理方法也可以。
這樣的想法實際上也被應用在 Python 中的許多場景,舉例來說:
unittest.assertRaises
方法可以幫助使用者單元測試一程式碼片段是否會跳出預期錯誤,但又不會讓程序中斷。unittest.subTest
方法可以幫助使用者強制執行子測試當中的所有測試,即使中間出錯也會執行到底。contextlib.suppress
方法可以幫助程序不中斷特定錯誤。
結論¶
理解 Python 的資源管理後,一方面可以讓我們在之後使用 with
的時候可以更有自信,另一方面也可以更加體會 Python 的一些機制、認識好用的內建套件、建立對 Python 更正確的認知、進而撰寫更漂亮的程式結構。