Python 的上下文管理器是怎么設計的?
花下貓語:最近,我在看 Python 3.10 版本的更新內容時,發現有一個關于上下文管理器的小更新,然后,突然發現上下文管理器的設計 PEP 竟然還沒人翻譯過!于是,我斷斷續續花了兩周時間,終于把這篇 PEP 翻譯出來了。如果你不了解什么是 PEP,可以先查看這篇《學習Python,怎能不懂點PEP呢?》,如果你也對翻譯 PEP 感興趣,歡迎加入 Github 上的 peps-cn 項目。
PEP原文 : https://www.python.org/dev/peps/pep-0343
PEP標題: PEP 343 – The “with” Statement
創建日期: 2005-05-13
合入版本: 2.5
譯者 :豌豆花下貓@Python貓公眾號
PEP翻譯計劃 :https://github.com/chinesehuazhou/peps-cn
摘要
本 PEP 提議在 Python 中新增一種"with"語句,可以取代常規的 try/finally 語句。
在本 PEP 中,上下文管理器提供__enter__() 和 __exit__() 方法,在進入和退出 with 語句體時,這倆方法分別會被調用。
本 PEP 最初由 Guido 以第一人稱編寫,隨后由 Nick Coghlan 根據 python-dev 上的討論,做出了更新補充。所有第一人稱的內容都出自于 Guido 的原文。
Python 的 alpha 版本發布周期暴露了本 PEP 以及相關文檔和實現[14]中的術語問題。直到 Python 2.5 的第一個 beta 版本發布時,本 PEP 才穩定下來。
是的,本文某些地方的動詞時態是混亂的。到現在為止,我們已經創作此 PEP 一年多了,所以,有些原本在未來的事情,現在已經成為過去了:)
介紹
經過對 PEP-340 及其替代方案的大量討論后,我決定撤銷 PEP-340,并提出了 PEP-310 的一個小變種。經過更多的討論后,我又添加了一種機制,可以使用 throw() 方法,在掛起的生成器中拋出異常,或者用一個 close() 方法拋出一個 GeneratorExitexception;這些想法最初是在 python-dev [2] 上提出的,并得到了普遍的認可。我還將關鍵字改為了“with”。
(Python貓注:PEP-340 也是 Guido 寫的,他最初用的關鍵字是“block”,后來改成了其它 PEP 提議的“with”。)
在本 PEP 被接受后,以下 PEP 由于重疊而被拒絕:
PEP-310,可靠的獲取/釋放對。這是 with 語句的原始提案。
PEP-319,Python 同步/異步代碼塊。通過提供合適的 with 語句控制器,本 PEP 可以涵蓋它的使用場景:對于’synchronize’,我們可以使用示例 1 中的"locking"模板;對于’asynchronize’,我們可以使用類似的"unlock"模板。我認為不必要給代碼塊加上“匿名的”鎖;事實上,應該盡可能地使用明確的互斥鎖。
PEP-340 和 PEP-346 也與本 PEP 重疊,但當本 PEP 被提交時,它們就自行撤銷了。
關于本 PEP 早期版本的一些討論,可以在 Python Wiki[3] 上查看。
動機與摘要
PEP-340(即匿名的 block 語句)包含了許多強大的創意:使用生成器作為代碼塊模板、給生成器添加異常處理和終結,等等。除了贊揚之外,它還被很多人所反對,他們不喜歡它是一個(潛在的)循環結構。這意味著塊語句中的 break 和 continue 可以中斷或繼續塊語句,即使它原本被當作非循環的資源管理工具。
但是,直到我讀了 Raymond Chen 對流量控制宏[1]的抨擊時,PEP-340 才走入了末路。Raymond 令人信服地指出,在宏中藏有流程控制會讓你的代碼變得難以捉摸,我覺得他的論點不僅適用于 C,同樣適用于 Python。我意識到,PEP-340 的模板可以隱藏各種控制流;例如,它的示例 4 (auto_retry())捕獲了異常,并將代碼塊重復三次。
然而,在我看來,PEP-310 的 with 語句并沒有隱藏控制流:雖然 finally 代碼部分會暫時掛起控制流,但到了最后,控制流會恢復,就好像 finally 子句根本不存在一樣。
在 PEP-310 中,它大致提出了以下的語法("VAR ="部分是可選的):
with VAR = EXPR: BLOCK
大致可以理解為:
VAR = EXPR VAR.__enter__() try: BLOCK finally: VAR.__exit__()
現在考慮這個例子:
with f = open("/etc/passwd"): BLOCK1 BLOCK2
在上例中,第一行就像是一個“if True”,我們知道如果 BLOCK1 在執行時沒有拋異常,那么 BLOCK2 將會被執行;如果 BLOCK1 拋出異常,或執行了非局部的 goto (即 break、continue 或 return),那么 BLOCK2 就不會被執行。也就是說,with 語句所加入的魔法并不會影響到這種流程邏輯。
(你可能會問,如果__exit__() 方法因為 bug 導致拋異常怎么辦?那么一切都完了——但這并不比其他情況更糟;異常的本質就是,它們可能發生在任何地方,你只能接受這一點。即便你寫的代碼沒有 bug,KeyboardInterrupt 異常仍然會導致程序在任意兩個虛擬機操作碼之間退出。)
這個論點幾乎讓我采納了 PEP-310,但是, PEP-340 還有一個亮點讓我不忍放棄:使用生成器作為某些抽象化行為的“模板”,例如獲取及釋放一個鎖,或者打開及關閉一個文件,這是一種很強大的想法,通過該 PEP 的例子就能看得出來。
受到 Phillip Eby 對 PEP-340 的反提議(counter-proposal)的啟發,我嘗試創建一個裝飾器,將合適的生成器轉換為具有必要的__enter__() 和 __exit__() 方法的對象。我在這里遇到了一個障礙:雖然這對于鎖的例子來說并不太難,但是對于打開文件的例子,卻不可能做到這一點。我的想法是像這樣定義模板:
@contextmanager def opening(filename): f = open(filename) try: yield f finally: f.close()
并這樣使用它:
with f = opening(filename): ...read data from f...
問題是在 PEP-310 中,EXPR 的調用結果直接分配給 VAR,然后 VAR 的__exit__() 方法會在 BLOCK1 退出時被調用。但是這里,VAR 顯然需要接收打開的文件,這意味著__exit__() 必須是文件對象的一個方法。
雖然這可以使用代理類來解決,但會很別扭,同時我還意識到,只需做出一個小小的轉變,就能輕輕松松地寫出所需的裝飾器:讓 VAR 接收__enter__() 方法的調用結果,接著保存 EXPR 的值,以便最后調用它的__exit__() 方法。
然后,裝飾器可以返回一個包裝器的實例,其__enter__() 方法調用生成器的 next() 方法,并返回 next() 所返回的值;包裝器實例的__exit__() 方法再次調用 next(),但期望它拋出 StopIteration。(詳細信息見下文的生成器裝飾器部分。)
因此,最后一個障礙便是 PEP-310 語法:
with VAR = EXPR: BLOCK1
這是有欺騙性的,因為 VAR 不接收 EXPR 的值。借用 PEP-340 的語法,很容易改成:
with EXPR as VAR: BLOCK1
在其他的討論中,人們真的很喜歡能夠“看到”生成器中的異常,盡管僅僅是為了記日志;生成器不允許產生(yield)其它的值,因為 with 語句不應該作為循環使用(引發不同的異常是勉強可以接受的)。
為了做到這點,我建議為生成器提供一個新的 throw() 方法,該方法以通常的方式接受 1 到 3 個參數(類型、值、回溯),表示一個異常,并在生成器掛起的地方拋出。
一旦我們有了這個,下一步就是添加另一個生成器方法 close(),它用一個特殊的異常(即 GeneratorExit)調用 throw(),可以令生成器退出。有了這個,在生成器被當作垃圾回收時,可以讓程序自動調用 close()。
最后,我們可以允許在 try-finally 語句中使用 yield 語句,因為我們現在可以保證 finally 子句必定被執行。關于終結(finalization)的常見注意事項——進程可能會在沒有終結任何對象的情況下突然被終止,而這些對象可能會因程序的周期或內存泄漏而永遠存活(在 Python 的實現中,周期或內存泄漏會由 GC 妥善處理)。
請注意,在使用完生成器對象后,我們不保證會立即執行 finally 子句,盡管在 CPython 中是這樣實現的。這類似于自動關閉文件:像 CPython 這樣的引用計數型解釋器,它會在最后一個引用消失時釋放一個對象,而使用其他 GC 算法的解釋器不保證也是如此。這指的是 Jython、IronPython,可能包括運行在 Parrot 上的 Python。
(關于對生成器所做的更改,可以在 PEP-342 中找到細節,而不是在當前 PEP 中。)
用例
請參閱文檔末尾的示例部分。
規格說明:'with’語句
提出了一種新的語句,語法如下:
with EXPR as VAR: BLOCK
在這里,“with”和“as”是新的關鍵字;EXPR 是任意一個表達式(但不是表達式列表),VAR 是一個單一的賦值目標。它不能是以逗號分隔的變量序列,但可以是以圓括號包裹的以逗號分隔的變量序列。(這個限制使得將來的語法擴展可以出現多個逗號分隔的資源,每個資源都有自己的可選 as 子句。)
“as VAR”部分是可選的。
上述語句可以被翻譯為:
mgr = (EXPR) exit = type(mgr).__exit__ # Not calling it yet value = type(mgr).__enter__(mgr) exc = True try: try: VAR = value # Only if "as VAR" is present BLOCK except: # The exceptional case is handled here exc = False if not exit(mgr, *sys.exc_info()): raise # The exception is swallowed if exit() returns true finally: # The normal and non-local-goto cases are handled here if exc: exit(mgr, None, None, None)
在這里,小寫變量(mgr、exit、value、exc)是內部變量,用戶不能訪問;它們很可能是由特殊的寄存器或堆棧位置來實現。
上述詳細的翻譯旨在說明確切的語義。解釋器會按照順序查找相關的方法(__exit__、__enter__),如果沒有找到,將引發 AttributeError。類似地,如果任何一個調用引發了異常,其效果與上述代碼中的效果完全相同。
最后,如果 BLOCK 包含 break、continue 或 return 語句,__exit__() 方法就會被調用,帶三個 None 參數,就跟 BLOCK 正常執行完成一樣。(也就是說,__exit__() 不會將這些“偽異常”視為異常。)
如果語法中的"as VAR"部分被省略了,則翻譯中的"VAR ="部分也要被忽略(但 mgr.__enter__() 仍然會被調用)。
mgr.__exit__() 的調用約定如下。如果 finally 子句是通過 BLOCK 的正常完成或通過非局部 goto(即 BLOCK 中的 break、continue 或 return 語句)到達,則使用三個 None 參數調用mgr.__exit__()。如果 finally 子句是通過 BLOCK 引發的異常到達,則使用異常的類型、值和回溯這三個參數調用 mgr.__exit__()。
重要:如果 mgr.__exit__() 返回“true”,則異常將被“吞滅”。也就是說,如果返回"true",即便在 with 語句內部發生了異常,也會繼續執行 with 語句之后的下一條語句。然而,如果 with 語句通過非局部 goto (break、continue 或 return)跳出,則這個非局部返回將被重置,不管 mgr.__exit__() 的返回值是什么。這個細節的動機是使 mgr.__exit__() 能夠吞咽異常,而不使異常產生影響(因為默認的返回值 None為 false,這會導致異常被重新 raise)。吞下異常的主要用途是使編寫 @contextmanager 裝飾器成為可能,這樣被裝飾的生成器中的 try/except 代碼塊的行為就好像生成器的主體在 with-語句里內聯展開了一樣。
之所以將異常的細節傳給__exit__(),而不用 PEP -310 中不帶參數的__exit__(),原因是考慮到下面例子 3 的 transactional()。該示例會根據是否發生異常,從而決定提交或回滾事務。我們沒有用一個 bool 標志區分是否發生異常,而是傳了完整的異常信息,目的是可以記錄異常日志。依賴于 sys.exc_info() 獲取異常信息的提議被拒絕了;因為 sys.exc_info() 有著非常復雜的語義,它返回的異常信息完全有可能是很久之前就捕獲的。有人還提議添加一個布爾值,用于區分是到達 BLOCK 結尾,還是非局部 goto。這因為過于復雜和不必要而被拒絕;對于數據庫事務回滾,非局部 goto 應該被認為是正常的。
為了促進 Python 代碼中上下文的鏈接作用,__exit__() 方法不應該繼續 raise 傳遞給它的錯誤。在這種情況下,__exit__() 方法的調用者應該負責處理 raise。
這樣,如果調用者想知道__exit__() 是否調用失敗(而不是在傳出原始錯誤之前就完成清理),它就可以自己判斷。
如果__exit__() 沒有返回錯誤,那么就可以將__exit__() 方法本身解釋為成功(不管原始錯誤是被傳播還是抑制)。
然而,如果__exit__() 向其調用者傳播了異常,這就意味著__exit__() 本身已經失敗。因此,__exit__() 方法應該避免引發錯誤,除非它們確實失敗了。(允許原始錯誤繼續并不是失敗。)
過渡計劃
在 Python 2.5 中,新語法需要通過 future 引入:
from __future__ import with_statement
它會引入’with’和’as’關鍵字。如果沒有導入,使用’with’或’as’作為標識符時,將導致報錯。
在 Python 2.6 中,新語法總是生效的,'with’和’as’已經是關鍵字。
生成器裝飾器
隨著 PEP-342 被采納,我們可以編寫一個裝飾器,令其使用只 yield 一次的生成器來控制 with 語句。這是一個裝飾器的粗略示例:
class GeneratorContextManager(object): def __init__(self, gen): self.gen = gen def __enter__(self): try: return self.gen.next() except StopIteration: raise RuntimeError("generator didn't yield") def __exit__(self, type, value, traceback): if type is None: try: self.gen.next() except StopIteration: return else: raise RuntimeError("generator didn't stop") else: try: self.gen.throw(type, value, traceback) raise RuntimeError("generator didn't stop after throw()") except StopIteration: return True except: # only re-raise if it's *not* the exception that was # passed to throw(), because __exit__() must not raise # an exception unless __exit__() itself failed. But # throw() has to raise the exception to signal # propagation, so this fixes the impedance mismatch # between the throw() protocol and the __exit__() # protocol. # if sys.exc_info()[1] is not value: raise def contextmanager(func): def helper(*args, **kwds): return GeneratorContextManager(func(*args, **kwds)) return helper
這個裝飾器可以這樣使用:
@contextmanager def opening(filename): f = open(filename) # IOError is untouched by GeneratorContext try: yield f finally: f.close() # Ditto for errors here (however unlikely)
這個裝飾器的健壯版本將會加入到標準庫中。
標準庫中的上下文管理器
可以將__enter__() 和__exit__() 方法賦予某些對象,如文件、套接字和鎖,這樣就不用寫:
with locking(myLock): BLOCK
而是簡單地寫成:
with myLock: BLOCK
我想我們應該謹慎對待它;它可能會導致以下的錯誤:
f = open(filename) with f: BLOCK1 with f: BLOCK2
它可能跟你想的不一樣(在進入 block2 之前,f 已經關閉了)。
另一方面,這樣的錯誤很容易診斷;例如,當第二個 with 語句再調用 f.__enter__() 時,上面的生成器裝飾器將引發 RuntimeError。如果在一個已關閉的文件對象上調用__enter__,則可能引發類似的錯誤。
在 Python 2.5中,以下類型被標識為上下文管理器:
- file - thread.LockType - threading.Lock - threading.RLock - threading.Condition - threading.Semaphore - threading.BoundedSemaphore
還將在 decimal 模塊添加一個上下文管理器,以支持在 with 語句中使用本地的十進制算術上下文,并在退出 with 語句時,自動恢復原始上下文。
標準術語
本 PEP 提議將由__enter__() 和 __exit__() 方法組成的協議稱為“上下文管理器協議”,并將實現該協議的對象稱為“上下文管理器”。[4]
緊跟著 with 關鍵字的表達式被稱為“上下文表達式”,該表達式提供了上下文管理器在with 代碼塊中所建立的運行時環境的主要線索。
目前為止, with 語句體中的代碼和 as 關鍵字后面的變量名(一個或多個)還沒有特殊的術語。可以使用一般的術語“語句體”和“目標列表”,如果這些術語不清晰,可以使用“with”或“with statement”作為前綴。
考慮到可能存在 decimal 模塊的算術上下文這樣的對象,因此術語“上下文”是有歧義的。如果想要更加具體的話,可以使用術語“上下文管理器”,表示上下文表達式所創建的具體對象;使用術語“運行時上下文”或者(最好是)“運行時環境”,表示上下文管理器所做出的實際狀態的變更。當簡單地討論 with 語句的用法時,歧義性無關緊要,因為上下文表達式完全定義了對運行時環境所做的更改。當討論 with 語句本身的機制以及如何實際實現上下文管理器時,這些術語的區別才是重要的。
緩存上下文管理器
許多上下文管理器(例如文件和基于生成器的上下文)都是一次性的對象。一旦__exit__() 方法被調用,上下文管理器將不再可用(例如:文件已經被關閉,或者底層生成器已經完成執行)。
對于多線程代碼,以及嵌套的 with 語句想要使用同一個上下文管理器,最簡單的方法是給每個 with 語句一個新的管理器對象。并非巧合的是,標準庫中所有支持重用的上下文管理器都來自 threading 模塊——它們都被設計用來處理由線程和嵌套使用所產生的問題。
這意味著,為了保存帶有特定初始化參數(為了用在多個 with 語句)的上下文管理器,通常需要將它存儲在一個無參數的可調用對象,然后在每個語句的上下文表達式中調用,而不是直接把上下文管理器緩存起來。
如果此限制不適用,在受影響的上下文管理器的文檔中,應該清楚地指出這一點。
解決的問題
以下的問題經由 BDFL 的裁決而解決(并且在 python-dev 上沒有重大的反對意見)。
1、當底層的生成器-迭代器行為異常時,GeneratorContextManager 應該引發什么異常?下面引用的內容是 Guido 為本 PEP及 PEP-342 (見[8])中生成器的 close() 方法選擇 RuntimeError 的原因:“我不愿意只是為了它而引入一個新的異常類,因為這不是我想讓人們捕獲的異常:我想讓它變成一個回溯(traceback),被程序員看到并且修復。因此,我認為它們都應該引發 RuntimeError。有一些引發 RuntimeError 的先例:Python 核心代碼在檢測到無限遞歸時,遇到未初始化的對象時(以及其它各種各樣的情況)。”
2、如果在with語句所涉及的類中沒有相關的方法,則最好是拋出AttributeError而不是TypeError。抽象對象C API引發TypeError而不是AttributeError,這只是歷史的一個偶然,而不是經過深思熟慮的設計決策[11]。
3、帶有__enter__ /__exit__方法的對象被稱為“上下文管理器”,將生成器函數轉化為上下文管理器工廠的是 contextlib.contextmanager 裝飾器。在 2.5版本發布期間,有人提議使用其它的叫法[16],但沒有足夠令人信服的理由。
拒絕的選項
在長達幾個月的時間里,對于是否要抑制異常(從而避免隱藏的流程控制),出現了一場令人痛苦的拉鋸戰,最終,Guido 決定要抑制異常[13]。
本 PEP 的另一個話題也引起了無休止的爭論,即是否要提供一個__context__() 方法,類似于可迭代對象的__iter__() 方法[5][7][9]。源源不斷的問題[10][13]在解釋它是什么、為什么是那樣、以及它是如何工作的,最終導致 Guido 完全拋棄了這個東西[15](這很讓人歡欣鼓舞!)
還有人提議直接使用 PEP-342 的生成器 API 來定義 with 語句[6],但這很快就不予考慮了,因為它會導致難以編寫不基于生成器的上下文管理器。
例子
基于生成器的示例依賴于 PEP-342。另外,有些例子是不實用的,因為標準庫中有現成的對象可以在 with 語句中直接使用,例如 threading.RLock。
例子中那些函數名所用的時態并不是隨意的。過去時態(“-ed”)的函數指的是在__enter__方法中執行,并在__exit__方法中反執行的動作。進行時態("-ing")的函數指的是準備在__exit__方法中執行的動作。
1、一個鎖的模板,在開始時獲取,在離開時釋放:
@contextmanager def locked(lock): lock.acquire() try: yield finally: lock.release()
使用如下:
with locked(myLock): # Code here executes with myLock held. The lock is # guaranteed to be released when the block is left (even # if via return or by an uncaught exception).
2、一個打開文件的模板,確保當代碼被執行后,文件會被關閉:
@contextmanager def opened(filename, mode="r"): f = open(filename, mode) try: yield f finally: f.close()
使用如下:
with opened("/etc/passwd") as f: for line in f: print line.rstrip()
3、一個數據庫事務的模板,用于提交或回滾:
@contextmanager def transaction(db): db.begin() try: yield None except: db.rollback() raise else: db.commit()
4、不使用生成器,重寫例子 1:
class locked: def __init__(self, lock): self.lock = lock def __enter__(self): self.lock.acquire() def __exit__(self, type, value, tb): self.lock.release()
(這個例子很容易被修改來實現其他相對無狀態的例子;這表明,如果不需要保留特殊的狀態,就不必要使用生成器。)
5、臨時重定向 stdout:
@contextmanager def stdout_redirected(new_stdout): save_stdout = sys.stdout sys.stdout = new_stdout try: yield None finally: sys.stdout = save_stdout
使用如下:
with opened(filename, "w") as f: with stdout_redirected(f): print "Hello world"
當然,這不是線程安全的,但是若不用管理器的話,本身也不是線程安全的。在單線程程序(例如腳本)中,這種做法很受歡迎。
6、opened() 的一個變體,也返回一個錯誤條件:
@contextmanager def opened_w_error(filename, mode="r"): try: f = open(filename, mode) except IOError, err: yield None, err else: try: yield f, None finally: f.close()
使用如下:
with opened_w_error("/etc/passwd", "a") as (f, err): if err: print "IOError:", err else: f.write("guido::0:0::/:/bin/sh\n")
7、另一個有用的操作是阻塞信號。它的用法是這樣的:
import signal with signal.blocked(): # code executed without worrying about signals
它的參數是可選的,表示要阻塞的信號列表;在默認情況下,所有信號都被阻塞。具體實現就留給讀者作為練習吧。
8、此特性還有一個用途是 Decimal 上下文。下面是 Michael Chermside 發布的一個簡單的例子:
import decimal @contextmanager def extra_precision(places=2): c = decimal.getcontext() saved_prec = c.prec c.prec += places try: yield None finally: c.prec = saved_prec
示例用法(摘自 Python 庫參考文檔):
def sin(x): "Return the sine of x as measured in radians." with extra_precision(): i, lasts, s, fact, num, sign = 1, 0, x, 1, x, 1 while s != lasts: lasts = s i += 2 fact *= i * (i-1) num *= x * x sign *= -1 s += num / fact * sign # The "+s" rounds back to the original precision, # so this must be outside the with-statement: return +s
9、下面是 decimal 模塊的一個簡單的上下文管理器:
@contextmanager def localcontext(ctx=None): """Set a new local decimal context for the block""" # Default to using the current context if ctx is None: ctx = getcontext() # We set the thread context to a copy of this context # to ensure that changes within the block are kept # local to the block. newctx = ctx.copy() oldctx = decimal.getcontext() decimal.setcontext(newctx) try: yield newctx finally: # Always restore the original context decimal.setcontext(oldctx)
示例用法:
from decimal import localcontext, ExtendedContext def sin(x): with localcontext() as ctx: ctx.prec += 2 # Rest of sin calculation algorithm # uses a precision 2 greater than normal return +s # Convert result to normal precision def sin(x): with localcontext(ExtendedContext): # Rest of sin calculation algorithm # uses the Extended Context from the # General Decimal Arithmetic Specification return +s # Convert result to normal context
10、一個通用的“對象關閉”上下文管理器:
class closing(object): def __init__(self, obj): self.obj = obj def __enter__(self): return self.obj def __exit__(self, *exc_info): try: close_it = self.obj.close except AttributeError: pass else: close_it()
這可以確保關閉任何帶有 close 方法的東西,無論是文件、生成器,還是其他東西。它甚至可以在對象并不需要關閉的情況下使用(例如,一個接受了任意可迭代對象的函數):
# emulate opening(): with closing(open("argument.txt")) as contradiction: for line in contradiction: print line # deterministically finalize an iterator: with closing(iter(data_source)) as data: for datum in data: process(datum)
(Python 2.5 的 contextlib 模塊包含了這個上下文管理器的一個版本)
11、PEP-319 給出了一個用例,它也有一個 release() 上下文,能臨時釋放先前獲得的鎖;這個用例跟前文的例子 4 很相似,只是交換了 acquire() 和 release() 的調用:
class released: def __init__(self, lock): self.lock = lock def __enter__(self): self.lock.release() def __exit__(self, type, value, tb): self.lock.acquire()
示例用法:
with my_lock: # Operations with the lock held with released(my_lock): # Operations without the lock # e.g. blocking I/O # Lock is held again here
12、一個“嵌套型”上下文管理器,自動從左到右嵌套所提供的上下文,可以避免過度縮進:
@contextmanager def nested(*contexts): exits = [] vars = [] try: try: for context in contexts: exit = context.__exit__ enter = context.__enter__ vars.append(enter()) exits.append(exit) yield vars except: exc = sys.exc_info() else: exc = (None, None, None) finally: while exits: exit = exits.pop() try: exit(*exc) except: exc = sys.exc_info() else: exc = (None, None, None) if exc != (None, None, None): # sys.exc_info() may have been # changed by one of the exit methods # so provide explicit exception info raise exc[0], exc[1], exc[2]
示例用法:
with nested(a, b, c) as (x, y, z): # Perform operation
等價于:
with a as x: with b as y: with c as z: # Perform operation
(Python 2.5 的 contextlib 模塊包含了這個上下文管理器的一個版本)
參考實現
在 2005 年 6 月 27 日的 EuroPython 會議上,Guido 首次采納了這個 PEP。之后它添加了__context__方法,并被再次采納。此 PEP 在 Python 2.5 a1 子版本中實現,__context__() 方法在 Python 2.5b1 中被刪除。
致謝
許多人對這個 PEP 中的想法和概念作出了貢獻,包括在 PEP-340 和 PEP-346 的致謝中提到的所有人。
另外,還要感謝(排名不分先后):Paul Moore, Phillip J. Eby, Greg Ewing, Jason Orendorff, Michael Hudson, Raymond Hettinger, Walter D?rwald, Aahz, Georg Brandl, Terry Reedy, A.M. Kuchling, Brett Cannon,以及所有參與了 python-dev 討論的人。
參考鏈接
[1] Raymond Chen’s article on hidden flow controlhttps://devblogs.microsoft.com/oldnewthing/20050106-00/?p=36783
[2] Guido suggests some generator changes that ended up in PEP 342https://mail.python.org/pipermail/python-dev/2005-May/053885.html
[3] Wiki discussion of PEP 343http://wiki.python.org/moin/WithStatement
[4] Early draft of some documentation for the with statementhttps://mail.python.org/pipermail/python-dev/2005-July/054658.html
[5] Proposal to add the with methodhttps://mail.python.org/pipermail/python-dev/2005-October/056947.html
[6] Proposal to use the PEP 342 enhanced generator API directlyhttps://mail.python.org/pipermail/python-dev/2005-October/056969.html
[7] Guido lets me (Nick Coghlan) talk him into a bad idea ;)https://mail.python.org/pipermail/python-dev/2005-October/057018.html
[8] Guido raises some exception handling questionshttps://mail.python.org/pipermail/python-dev/2005-June/054064.html
[9] Guido answers some questions about the context methodhttps://mail.python.org/pipermail/python-dev/2005-October/057520.html
[10] Guido answers more questions about the context methodhttps://mail.python.org/pipermail/python-dev/2005-October/057535.html
[11] Guido says AttributeError is fine for missing special methodshttps://mail.python.org/pipermail/python-dev/2005-October/057625.html
[12] Original PEP 342 implementation patchhttp://sourceforge.net/tracker/index.php?func=detail&aid=1223381&group_id=5470&atid=305470
[13] (1, 2) Guido restores the ability to suppress exceptionshttps://mail.python.org/pipermail/python-dev/2006-February/061909.html
[14] A simple question kickstarts a thorough review of PEP 343https://mail.python.org/pipermail/python-dev/2006-April/063859.html
[15] Guido kills the context() methodhttps://mail.python.org/pipermail/python-dev/2006-April/064632.html
[16] Proposal to use ‘context guard’ instead of 'context manager’https://mail.python.org/pipermail/python-dev/2006-May/064676.html
版權
本文檔已進入公共領域。
源文檔:https://github.com/python/peps/blob/master/pep-0343.txt
Python 任務調度
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。