Python with 陳述式,with 陳述式內容管理器介紹
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 部分
block
為with
陳述式的主體程式碼,需要使用某種空白字元進行縮排,以表示其歸屬於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
方法將引發例外狀況。
# 檔案物件的 __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
陳述式內容管理器CM
,CM
的__enter__
方法用於儲存模組變數students
中的串列,CM
的__exit__
方法用於恢復模組變數students
中的串列。
# 模組變數 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
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
陳述式包含了兩個內容管理器,我們姑且稱他們為A
和B
。對於__enter__
方法,將按照A
,B
的順序被呼叫,對於__exit__
方法,將按照B
,A
的順序被呼叫。
# 自訂 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
,例外狀況將按照B
,A
的順序傳播。
# …
# 例外狀況將按照 B,A 的順序傳播
with CM('A'), CM('B'):
num = 1 / 0
__exit__ B <class 'ZeroDivisionError'>
__exit__ A <class 'ZeroDivisionError'>
…
ZeroDivisionError: division by zero