目錄
基本概念
上下文管理協議(Context Management Protocol)
包含方法 __enter__() 和 __exit__(),支持該協議的對象要實現這兩個方法。
上下文管理器(Context Manager):
支持上下文管理協議的對象,這種對象實現了__enter__() 和 __exit__() 方法。上下文管理器定義執行 with 語句時要建立的運行時上下文,負責執行 with 語句塊上下文中的進入與退出操作。通常使用 with 語句調用上下文管理器,也可以通過直接調用其方法來使用。
運行時上下文(runtime context):
由上下文管理器創建,通過上下文管理器的 __enter__() 和__exit__() 方法實現,__enter__() 方法在語句體執行之前進入運行時上下文,__exit__() 在語句體執行完后從運行時上下文退出。with 語句支持運行時上下文這一概念。
上下文表達式(Context Expression):
with 語句中跟在關鍵字 with 之后的表達式,該表達式要返回一個上下文管理器對象。
語句體(with-body):
with 語句包裹起來的代碼塊,在執行語句體前會調用上下文管理器的 __enter__() 方法,執行完語句體之后會執行 __exit__() 方法。
關於 Python 中 with 語句的詳細說明:PEP 343
with 語句用上下文管理器定義的方法包裹一段代碼的執行,等價於簡單版的try...except...finally語句。with語句的主要針對的情境:不論一個代碼塊的執行過程是否出現異常,都要在結束的時候執行一些操作(比如清理)。
with語句語法:
with_stmt ::= "with" with_item ("," with_item)* ":" suite with_item ::= expression ["as" target]
或:
with expression [as variable]: with-block
expression應該返回一個支持“上下文管理協議”的對象,如果 as 分句存在的話,這個對象的返回值會被賦給 as 后面的變量名。
第一種語法表示中with語句的執行流程:
- 計算上下文表達式 (with_item 中給出的表達式) ,獲得一個上下文管理器,該上下文管理器包括__enter__() 和 __exit__()方法;
- 加載該上下文管理器的 __exit__() 方法留待以后使用;
- 調用上下文管理器的 __enter__() 方法;
- 如果 with 語句中的 with_item 指定了目標別名(as 分句后面的變量名),__enter__() 的返回值被賦給這個目標別名;
*注
with 語句確保只要 __enter__() 正常返回,那么 __exit__()一定會被調用。因此如果在將值賦給目標別名時發生錯誤,該錯誤將被當做發生在suite中。可以看下面的第6步。
5. with語句中嵌套的 suite 部分被執行(第二種表示中的 with-block 部分);
6. 上下文管理器的 __exit__() 方法被調用,如果是異常造成 suite(with-block) 部分退出,異常的類型、值和回溯都被當做參數傳給 __exit__(type, value, traceback) 方法,這三個值和sys.exc_info的返回值相同。如果suite(with-block) 部分沒有拋出異常,__exit__()的三個參數都是 None。
如果 suite 部分是由於異常導致的退出,且__exit__()方法的返回值是false,異常將被重舉;如果返回值是真,異常將被終止,with 語句后的代碼繼續執行。
如果 suite 部分是由於不是異常的其他原因導致的退出,__exit__()方法的返回值被忽視,執行在退出發生的地方繼續。
單個上下文管理器示例:
with open(r'C:\misc\data') as myfile: for line in myfile: print(line) #...more code...
Python中的文件對象包含會在with代碼段后自動關閉文件對象的上下文管理器,所以即便 for 循環中拋出處理文件對象時的異常,也能保證 myfile 引用的對象被正常關閉。
如果這段代碼使用 try...finally...語句改寫:
myfile = open(r'C:\misc\data') try: for line in myfile: print(line) #...more code... finally: myfile.close()
可見使用 with 語句能夠簡化代碼。
多個上下文管理器被處理的方式就像多個 with 語句嵌套執行一樣:
with A() as a, B() as b: suite
等價於:
with A() as a: with B() as b: suite
多個上下文管理器示例:
with open('data') as fin, open(''res', 'w') as fout: for line in fin: if 'some key' in line: fout.write(line)
二、上下文管理器類型
context manager 是Python中 with 語句執行時用來定義運行時上下文的對象,上下文管理器控制着 進 / 出 運行時上下文的功能,上下文管理器通常由 with 語句觸發,也可以直接通過調用他們的方法來使用他們。上下文管理器的通常用於保存和恢復各式各樣的全局狀態、加解鎖資源和關閉打開的文件等等。
Python的 with 語句支持由上下文管理器定義的運行時上下文,由兩個方法來實現,這兩個方法允許用戶在自定義的類中定義運行時上下文,執行流程在 with 語句開始前進入上下文,當 with 語句結束后退出。
上下文管理器必須提供一對方法:
contextmanager.__enter__()
進入運行時上下文,要么返回該對象,要么返回一個與運行時上下文相關的對象,如果有 as 分句的話,with 語句將會把該方法的返回值和 as 分句指定的目標(別名)進行綁定。
比如文件對象在__enter__()里返回自己,這樣 open() 函數可以被當做環境表達式在一個 with 語句中使用。
- 另一種情形中, decimal.localcontext()返回一個相關的對象。上下文管理器將活躍的小數上下文設置為初始小數上下文的拷貝,然后返回拷貝后的引用。這樣可以在 with 語句中修改當前的小數上下文而不影響with 語句外的代碼。
- contextmanager.__exit__(exc_type, exc_val, exc_tb)
- 退出運行時上下文,參數是 with 代碼塊中拋出的異常的詳細信息,返回一個Bool型的標識符,該標識符說明上下文管理器能否處理異常:如果上下文管理器可以處理這個異常,__exit__() 應當返回一個true值來指示不需要傳播這個異常;如果返回false,就會導致__exit__() 返回后重新拋出這個異常, 如果上下文不是因為異常而退出,則三個參數都是 None。
- 該方法如果返回True ,說明上下文管理器可以處理異常,使得 with 語句終止異常傳播,然后直接執行 with 語句后的語句;否則異常會在該方法調用結束后正常傳播,該方法執行時發生的異常會替代所有其他在執行 with 代碼塊時出現的異常。
- 在__exit__()中不需要顯式重舉傳入的異常,只需要返回 false 表明 __exit__() 正常執行且傳入的異常需要重舉即可,重舉傳入__exit__()方法的異常是調用__exit__()的函數的工作。這種機制便於上下文管理器檢測 __exit__() 方法是否真的執行失敗。
- Python定義了若干上下文管理器,Python的 generator 類型和裝飾器 contextlib.contextmanager 提供了實現上下文管理器協議的手段,如果一個生成器函數被裝飾器 contextlib.contextmanager 所裝飾,它將會返回一個實現了必要的 __enter__() 和 __exit__() 方法的上下文管理器,而不是普通的迭代器!
Note that there is no specific slot for any of these methods in the type structure for Python objects in the Python/C API. Extension types wanting to define these methods must provide them as a normal Python accessible method. Compared to the overhead of setting up the runtime context, the overhead of a single class dictionary lookup is negligible.
自定義上下文管理器示例:
class TraceBlock(object): def message(self, arg): print('running' + arg) def __enter__(self): print('starting with block') return self def __exit__(self, exc_type, exc_value, exc_tb): if exc_type is None: print('exited normally\n') else: print('raise an exception! ' + str(exc_type)) return False #Propagate if __name__ == '__main__': with TraceBlock() as action: action.message('test1') print('reached') with TraceBlock() as action: action.message('test2') raise TypeError print ('not reached')
采用“二、上下文管理器類型”中說明的傳統方法即編寫一個包含__enter__()和__exit__()方法的類來創建上下文管理器並不難。不過有些時候,對於很少的上下文來說,完全編寫所有代碼會是額外的負擔。在這些情況下,可以使用裝飾器 contextlib.contextmanager() 將一個生成器函數轉換為上下文管理器。
contextlib模塊提供對 with 語句的支持:
contextlib.contextmanager(func)
裝飾器,用來裝飾一個生成器函數,使其成為一個上下文管理器。通過該裝飾器可以方便地定義上下文管理器,不必創建一個類或單獨指定__enter__() 和 __exit__() 方法。
示例
(該例子不應該用於實際生成HTML!):
from contextlib import contextmanager @contextmanager def tag(name): print "<%s>" % name yield print "</%s>" % name >>> with tag("h1"): ... print "foo" ... <h1> foo </h1>
上下文管理器的 __enter__() 和 __exit__() 方法由 @contextmanager 負責提供,不再是之前的迭代子。被裝飾的生成器函數只能產生一個值,否則會導致異常 RuntimeError;如果使用了 as 子句的話,產生的值會賦值給 as 子句中的 target。
生成器函數中 yield 之前的語句在 __enter__() 方法中執行;yield 之后的語句在 __exit__() 中執行;yield 產生的值賦給 as 子句中的 variable 變量。
生成器 yield 的地方,切換到執行 with 語句中的代碼塊(with-block),生成器在代碼塊退出后繼續執行。
如果 with 代碼塊中有未被處理的異常,它會被重舉到生成器函數中的 yield 處,因此可以在生成器函數中使用 try...except...finally 語句來處理被重舉的異常。
如果捕獲異常的目標是記錄日志或執行一些操作(而不是抑制它),生成器必須重舉異常。否則生成器上下文管理器將會告知 with 語句說異常已經被處理了,但是一旦執行到 with 語句后的代碼,異常會立即繼續。
需要注意的是,@contextmanager 只是省略了 __enter__() / __exit__() 的編寫,實際意義是減少了代碼量。
但裝飾器@contextmanager並不負責資源的“獲取”和“清理”工作(需要開發者自己實現)。“獲取”操作需要定義在 yield 語句之前,“清理”操作需要定義 yield 語句之后,這樣 with 語句在執行 __enter__() / __exit__() 方法時會執行這些語句以獲取/釋放資源,即生成器函數中需要實現必要的邏輯控制,包括資源訪問出現錯誤時拋出適當的異常。
示例:
from contextlib import contextmanager @contextmanager def make_context() : print 'enter' try : yield {} except RuntimeError, err : print 'error' , err finally : print 'exit' with make_context() as value : print value
with-block中拋出的異常將被重舉到生成器的 yield 處,在生成器中可以捕獲該異常。
contextlib.closing(thing)
返回一個上下文管理器,在完成代碼塊的執行時關閉參數 thing。等價於:
from contextlib import contextmanager @contextmanager def closing(thing): try: yield thing finally: thing.close()
可以這么使用:
from contextlib import closing import urllib with closing(urllib.urlopen('http://www.python.org')) as page: for line in page: print line
該例中,不用顯式地關閉 page。即使發生錯誤,也會在 with 代碼塊退出時執行 page.close()