Python with 陳述式,with 陳述式內容管理器介紹

閱讀 9:22·字數 2812·發佈 
Youtube 頻道
訂閱 133

Python with 陳述式

如果你希望能夠及時釋放重要的資源,以防止其因為某些情況(比如例外狀況)而始終處於被占用的狀態,或者希望臨時儲存目前狀態以便在稍微恢復,那麽 Python 提供的with陳述式可用於完成上述目標,該陳述式僅接受with陳述式內容管理器作為處理目標,其基本書寫格式如下。

with <expression>[ as <target>]:
    <block>

expression 部分

expression是一個運算結果為with陳述式內容管理器的運算式,比如,一個呼叫 Python 內建函式open獲得檔案物件的運算式(Python 檔案物件是有效的with陳述式內容管理器)。

target 部分

target是變數的名稱,該變數將儲存with陳述式內容管理器(即expression部分的運算結果)的__enter__方法的傳回值。變數名稱需要符合 Python 的識別碼規格,不能使用 Python 關鍵字或保留關鍵字。

block 部分

blockwith陳述式的主體程式碼,需要使用某種空白字元進行縮排,以表示其歸屬於with陳述式。

在 Pythonwith陳述式開始執行時,expression部分所傳回的with陳述式內容管理器的__enter__方法將被呼叫,你可以在該方法中儲存一些重要的資料或狀態。

在 Pythonwith陳述式結束執行時,with陳述式內容管理器的__exit__方法將被呼叫,這裏所說的陳述式結束包括正常結束和非正常結束(比如,block部分的程式碼出現了例外狀況)。由於,Pythonwith陳述式的內容管理器的__exit__方法總是被呼叫,因此__exit__方法可用於釋放一些重要的資源,或恢復之前儲存的資料或狀態。

Python with 陳述式的內容管理器的 __enter__ 方法可以傳回自身

Python 的with陳述式對內容管理器的__enter__方法的傳回值的型別沒有要求,因此,__enter__方法可以傳回內容管理器自身,以方便程式碼的編寫和閱讀。

下面,我們使用with陳述式來開啟檔案with.txt,由於 Python 檔案物件的__enter__方法傳回其自身,因此變數file指向檔案物件,可以使用他將內容寫入檔案。

with陳述式結束時,Python 檔案物件的__exit__方法會被呼叫,這將使檔案物件被關閉,再次呼叫其write方法將引發例外狀況。

with.py
# 檔案物件的 __enter__ 方法傳回自身
with open('with.txt', 'w', encoding='utf8') as file:
	file.write('with 陳述式')

# ERROR 檔案物件已經被關閉 file.write('又一條 with 陳述式')
ValueError: I/O operation on closed file.

Python with 陳述式的內容管理器

Pythonwith陳述式的內容管理器應該擁有__enter____exit__方法,如前所述,他們分別在with陳述式開始和結束時被呼叫,__enter__方法的傳回值將作為with…as陳述式所定義的變數的值。

__enter__(self)

如果 Pythonwith陳述式的主體程式碼引發了例外狀況並且沒有處理,那麽例外狀況的資訊將被傳遞給with陳述式內容管理器的__exit__方法的三個參數,如果此時__exit__方法傳回True或被視為True的值,那麽例外狀況將被抑製,後續程式碼可以在不處理例外狀況的情況下繼續執行。

如果 Pythonwith陳述式的主體程式碼沒有引發例外狀況或者例外狀況得到了處理,那麽with陳述式內容管理器的__exit__方法的三個參數將為None

__exit__(self, exc_type, exc_value, exc_tb)

exc_type 參數

exc_type參數是一個type物件,表示with陳述式的主體程式碼所引發的例外狀況的型別。

exc_value 參數

exc_value參數為with陳述式的主體程式碼所引發的例外狀況。

exc_tb 參數

exc_tb參數表示with陳述式的主體程式碼所引發的例外狀況的回溯型別。

不需要在 Python with 陳述式的內容管理器的 __exit__ 方法中再次擲回例外狀況

當 Pythonwith陳述式的主體程式碼引發了例外狀況且例外狀況沒有被處理時,不需要在with陳述式的內容管理器的__exit__方法中再次擲回例外狀況,只要確保__exit__方法沒有傳回True或被視為True的值,例外狀況便可繼續傳播,即with陳述式以外的程式碼應負責處理例外狀況。

下面,我們定義了自己的with陳述式內容管理器CMCM__enter__方法用於儲存模組變數students中的串列,CM__exit__方法用於恢復模組變數students中的串列。

with_cm.py
# 模組變數 students
students = ['小紅', '小黑']

# 自訂 with 陳述式內容管理器 CM class CM: def __enter__(self): # 將模組變數 students 中的串列儲存到 CM 中 global students self.students = students
# 將新的串列指派給模組變數 students students = ['小蘭', '小白'] return students
def __exit__(self, exc_type, exc_value, exc_tb): # 恢復模組變數 students 中原有的串列 global students students = self.students
# CM 會臨時改變模組變數 students with CM() as s: print(s, students)
print(students)
['小蘭', '小白'] ['小蘭', '小白']
['小紅', '小黑']

在下面的範例中,with陳述式引發了例外狀況ZeroDivisionError,該例外狀況的相關資訊被傳遞給內容管理器物件CM__exit__方法,由於__exit__方法傳回了True,因此例外狀況ZeroDivisionError將被抑製。

with_cm_exc.py
# 自訂 with 陳述式內容管理器 CM
class CM:
	def __enter__(self):
		pass
	def __exit__(self, exc_type, exc_value, exc_tb):
		print(exc_type, exc_value, exc_tb)
		# 傳回 True 後,例外狀況將被抑製
		return True

with CM(): # 例外狀況 ZeroDivisionError 被引發並且沒有得到處理 num = 1 / 0
<class 'ZeroDivisionError'> division by zero <traceback object at >

在 Python with 陳述式中包含多個內容管理器

一般情況下,Pythonwith陳述式只處理一個內容管理器,但如果有需要,你可以在with陳述式中包含多個內容管理器,並使用逗號,分隔運算式,比如expression1 as target1,expression2 as target2。當然,你還可以使用括弧()括住所有 Pythonwith陳述式內容管理器對應的運算式,這樣每一個運算式可單獨占用一行。需要指出,這裏的括弧()並不會建立 Python 元組。

如果 Pythonwith陳述式包含了多個內容管理器,那麽將按照順序依次呼叫他們的__enter__方法,並按照相反的順序呼叫他們的__exit__方法。

在下面的範例中,with陳述式包含了兩個內容管理器,我們姑且稱他們為AB。對於__enter__方法,將按照AB的順序被呼叫,對於__exit__方法,將按照BA的順序被呼叫。

with_cm_multi.py
# 自訂 with 陳述式內容管理器 CM
class CM:
	# CM 的建構子
	def __init__(self, name):
		self.name = name

# __enter__ 和 __exit__ 方法將顯示 CM 的名稱 def __enter__(self): print(f'__enter__ {self.name}') def __exit__(self, exc_type, exc_value, exc_tb): print(f'__exit__ {self.name} {exc_type}')
# 包含了兩個內容管理器 with ( CM('A'), CM('B') ): pass
__enter__ A
__enter__ B
__exit__ B None
__exit__ A None

例外狀況會按照從後向前的順序在 Python with 陳述式的多個內容管理器中傳播

除非例外狀況被處理或被__exit__方法抑製,否則他將按照從後向前的順序在 Pythonwith陳述式的多個內容管理器中傳播。

我們為上面的範例增加一些程式碼,在with陳述式中引發例外狀況ZeroDivisionError,例外狀況將按照BA的順序傳播。

with_cm_multi.py
# …
# 例外狀況將按照 B,A 的順序傳播
with CM('A'), CM('B'):
	num = 1 / 0
__exit__ B <class 'ZeroDivisionError'>
__exit__ A <class 'ZeroDivisionError'>

ZeroDivisionError: division by zero

程式碼

src/zh-hant/statements/with·codebeatme/python·GitHub