Python 的上下文管理器是怎樣設(shè)計(jì)的?
PEP原文 :https://www.python.org/dev/peps/pep-0343
PEP標(biāo)題:PEP 343 -- The "with" Statement
PEP作者:Guido van Rossum, Nick Coghlan
創(chuàng)建日期:2005-05-13
合入版本:2.5
譯者:豌豆花下貓@Python貓公眾號(hào)
PEP翻譯計(jì)劃:https://github.com/chinesehuazhou/peps-cn
摘要
本 PEP 提議在 Python 中新增一種"with"語(yǔ)句,可以取代常規(guī)的 try/finally 語(yǔ)句。
在本 PEP 中,上下文管理器提供__enter__() 和 __exit__() 方法,在進(jìn)入和退出 with 語(yǔ)句體時(shí),這倆方法分別會(huì)被調(diào)用。
作者的批注
本 PEP 最初由 Guido 以第一人稱編寫,隨后由 Nick Coghlan 根據(jù) python-dev 上的討論,做出了更新補(bǔ)充。所有第一人稱的內(nèi)容都出自于 Guido 的原文。
Python 的 alpha 版本發(fā)布周期暴露了本 PEP 以及相關(guān)文檔和實(shí)現(xiàn)[14]中的術(shù)語(yǔ)問(wèn)題。直到 Python 2.5 的第一個(gè) beta 版本發(fā)布時(shí),本 PEP 才穩(wěn)定下來(lái)。
是的,本文某些地方的動(dòng)詞時(shí)態(tài)是混亂的。到現(xiàn)在為止,我們已經(jīng)創(chuàng)作此 PEP 一年多了,所以,有些原本在未來(lái)的事情,現(xiàn)在已經(jīng)成為過(guò)去了:)
介紹
經(jīng)過(guò)對(duì) PEP-340 及其替代方案的大量討論后,我決定撤銷 PEP-340,并提出了 PEP-310 的一個(gè)小變種。經(jīng)過(guò)更多的討論后,我又添加了一種機(jī)制,可以使用 throw() 方法,在掛起的生成器中拋出異常,或者用一個(gè) close() 方法拋出一個(gè) GeneratorExitexception;這些想法最初是在 python-dev [2] 上提出的,并得到了普遍的認(rèn)可。我還將關(guān)鍵字改為了“with”。
(Python貓注:PEP-340 也是 Guido 寫的,他最初用的關(guān)鍵字是“block”,后來(lái)改成了其它 PEP 提議的“with”。)
在本 PEP 被接受后,以下 PEP 由于重疊而被拒絕:
PEP-310,可靠的獲取/釋放對(duì)。這是 with 語(yǔ)句的原始提案。 PEP-319,Python 同步/異步代碼塊。通過(guò)提供合適的 with 語(yǔ)句控制器,本 PEP 可以涵蓋它的使用場(chǎng)景:對(duì)于'synchronize',我們可以使用示例 1 中的"locking"模板;對(duì)于'asynchronize',我們可以使用類似的"unlock"模板。我認(rèn)為不必要給代碼塊加上“匿名的”鎖;事實(shí)上,應(yīng)該盡可能地使用明確的互斥鎖。
PEP-340 和 PEP-346 也與本 PEP 重疊,但當(dāng)本 PEP 被提交時(shí),它們就自行撤銷了。
關(guān)于本 PEP 早期版本的一些討論,可以在 Python Wiki[3] 上查看。
動(dòng)機(jī)與摘要
PEP-340(即匿名的 block 語(yǔ)句)包含了許多強(qiáng)大的創(chuàng)意:使用生成器作為代碼塊模板、給生成器添加異常處理和終結(jié),等等。除了贊揚(yáng)之外,它還被很多人所反對(duì),他們不喜歡它是一個(gè)(潛在的)循環(huán)結(jié)構(gòu)。這意味著塊語(yǔ)句中的 break 和 continue 可以中斷或繼續(xù)塊語(yǔ)句,即使它原本被當(dāng)作非循環(huán)的資源管理工具。
但是,直到我讀了 Raymond Chen 對(duì)流量控制宏[1]的抨擊時(shí),PEP-340 才走入了末路。Raymond 令人信服地指出,在宏中藏有流程控制會(huì)讓你的代碼變得難以捉摸,我覺(jué)得他的論點(diǎn)不僅適用于 C,同樣適用于 Python。我意識(shí)到,PEP-340 的模板可以隱藏各種控制流;例如,它的示例 4 (auto_retry())捕獲了異常,并將代碼塊重復(fù)三次。
然而,在我看來(lái),PEP-310 的 with 語(yǔ)句并沒(méi)有隱藏控制流:雖然 finally 代碼部分會(huì)暫時(shí)掛起控制流,但到了最后,控制流會(huì)恢復(fù),就好像 finally 子句根本不存在一樣。
在 PEP-310 中,它大致提出了以下的語(yǔ)法("VAR ="部分是可選的):
with VAR = EXPR:
BLOCK
大致可以理解為:
VAR = EXPR
VAR.__enter__()
try:
BLOCK
finally:
VAR.__exit__()
現(xiàn)在考慮這個(gè)例子:
with f = open("/etc/passwd"):
BLOCK1
BLOCK2
在上例中,第一行就像是一個(gè)“if True”,我們知道如果 BLOCK1 在執(zhí)行時(shí)沒(méi)有拋異常,那么 BLOCK2 將會(huì)被執(zhí)行;如果 BLOCK1 拋出異常,或執(zhí)行了非局部的 goto (即 break、continue 或 return),那么 BLOCK2 就不會(huì)被執(zhí)行。也就是說(shuō),with 語(yǔ)句所加入的魔法并不會(huì)影響到這種流程邏輯。
(你可能會(huì)問(wèn),如果__exit__() 方法因?yàn)?bug 導(dǎo)致拋異常怎么辦?那么一切都完了——但這并不比其他情況更糟;異常的本質(zhì)就是,它們可能發(fā)生在任何地方,你只能接受這一點(diǎn)。即便你寫的代碼沒(méi)有 bug,KeyboardInterrupt 異常仍然會(huì)導(dǎo)致程序在任意兩個(gè)虛擬機(jī)操作碼之間退出。)
這個(gè)論點(diǎn)幾乎讓我采納了 PEP-310,但是, PEP-340 還有一個(gè)亮點(diǎn)讓我不忍放棄:使用生成器作為某些抽象化行為的“模板”,例如獲取及釋放一個(gè)鎖,或者打開及關(guān)閉一個(gè)文件,這是一種很強(qiáng)大的想法,通過(guò)該 PEP 的例子就能看得出來(lái)。
受到 Phillip Eby 對(duì) PEP-340 的反提議(counter-proposal)的啟發(fā),我嘗試創(chuàng)建一個(gè)裝飾器,將合適的生成器轉(zhuǎn)換為具有必要的__enter__() 和 __exit__() 方法的對(duì)象。我在這里遇到了一個(gè)障礙:雖然這對(duì)于鎖的例子來(lái)說(shuō)并不太難,但是對(duì)于打開文件的例子,卻不可能做到這一點(diǎn)。我的想法是像這樣定義模板:
@contextmanager
def opening(filename):
f = open(filename)
try:
yield f
finally:
f.close()
并這樣使用它:
with f = opening(filename):
...read data from f...
問(wèn)題是在 PEP-310 中,EXPR 的調(diào)用結(jié)果直接分配給 VAR,然后 VAR 的__exit__() 方法會(huì)在 BLOCK1 退出時(shí)被調(diào)用。但是這里,VAR 顯然需要接收打開的文件,這意味著__exit__() 必須是文件對(duì)象的一個(gè)方法。
雖然這可以使用代理類來(lái)解決,但會(huì)很別扭,同時(shí)我還意識(shí)到,只需做出一個(gè)小小的轉(zhuǎn)變,就能輕輕松松地寫出所需的裝飾器:讓 VAR 接收__enter__() 方法的調(diào)用結(jié)果,接著保存 EXPR 的值,以便最后調(diào)用它的__exit__() 方法。
然后,裝飾器可以返回一個(gè)包裝器的實(shí)例,其__enter__() 方法調(diào)用生成器的 next() 方法,并返回 next() 所返回的值;包裝器實(shí)例的__exit__() 方法再次調(diào)用 next(),但期望它拋出 StopIteration。(詳細(xì)信息見下文的生成器裝飾器部分。)
因此,最后一個(gè)障礙便是 PEP-310 語(yǔ)法:
with VAR = EXPR:
BLOCK1
這是有欺騙性的,因?yàn)?VAR 不接收 EXPR 的值。借用 PEP-340 的語(yǔ)法,很容易改成:
with EXPR as VAR:
BLOCK1
在其他的討論中,人們真的很喜歡能夠“看到”生成器中的異常,盡管僅僅是為了記日志;生成器不允許產(chǎn)生(yield)其它的值,因?yàn)?with 語(yǔ)句不應(yīng)該作為循環(huán)使用(引發(fā)不同的異常是勉強(qiáng)可以接受的)。
為了做到這點(diǎn),我建議為生成器提供一個(gè)新的 throw() 方法,該方法以通常的方式接受 1 到 3 個(gè)參數(shù)(類型、值、回溯),表示一個(gè)異常,并在生成器掛起的地方拋出。
一旦我們有了這個(gè),下一步就是添加另一個(gè)生成器方法 close(),它用一個(gè)特殊的異常(即 GeneratorExit)調(diào)用 throw(),可以令生成器退出。有了這個(gè),在生成器被當(dāng)作垃圾回收時(shí),可以讓程序自動(dòng)調(diào)用 close()。
最后,我們可以允許在 try-finally 語(yǔ)句中使用 yield 語(yǔ)句,因?yàn)槲覀儸F(xiàn)在可以保證 finally 子句必定被執(zhí)行。關(guān)于終結(jié)(finalization)的常見注意事項(xiàng)——進(jìn)程可能會(huì)在沒(méi)有終結(jié)任何對(duì)象的情況下突然被終止,而這些對(duì)象可能會(huì)因程序的周期或內(nèi)存泄漏而永遠(yuǎn)存活(在 Python 的實(shí)現(xiàn)中,周期或內(nèi)存泄漏會(huì)由 GC 妥善處理)。
請(qǐng)注意,在使用完生成器對(duì)象后,我們不保證會(huì)立即執(zhí)行 finally 子句,盡管在 CPython 中是這樣實(shí)現(xiàn)的。這類似于自動(dòng)關(guān)閉文件:像 CPython 這樣的引用計(jì)數(shù)型解釋器,它會(huì)在最后一個(gè)引用消失時(shí)釋放一個(gè)對(duì)象,而使用其他 GC 算法的解釋器不保證也是如此。這指的是 Jython、IronPython,可能包括運(yùn)行在 Parrot 上的 Python。
(關(guān)于對(duì)生成器所做的更改,可以在 PEP-342 中找到細(xì)節(jié),而不是在當(dāng)前 PEP 中。)(Python貓注:我也曾翻譯過(guò) PEP-342,鏈接在此)
用例
請(qǐng)參閱文檔末尾的示例部分。
規(guī)格說(shuō)明:'with'語(yǔ)句
提出了一種新的語(yǔ)句,語(yǔ)法如下:
with EXPR as VAR:
BLOCK
在這里,“with”和“as”是新的關(guān)鍵字;EXPR 是任意一個(gè)表達(dá)式(但不是表達(dá)式列表),VAR 是一個(gè)單一的賦值目標(biāo)。它不能是以逗號(hào)分隔的變量序列,但可以是以圓括號(hào)包裹的以逗號(hào)分隔的變量序列。(這個(gè)限制使得將來(lái)的語(yǔ)法擴(kuò)展可以出現(xiàn)多個(gè)逗號(hào)分隔的資源,每個(gè)資源都有自己的可選 as 子句。)
“as VAR”部分是可選的。
上述語(yǔ)句可以被翻譯為:
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)是內(nèi)部變量,用戶不能訪問(wèn);它們很可能是由特殊的寄存器或堆棧位置來(lái)實(shí)現(xiàn)。
上述詳細(xì)的翻譯旨在說(shuō)明確切的語(yǔ)義。解釋器會(huì)按照順序查找相關(guān)的方法(__exit__、__enter__),如果沒(méi)有找到,將引發(fā) AttributeError。類似地,如果任何一個(gè)調(diào)用引發(fā)了異常,其效果與上述代碼中的效果完全相同。
最后,如果 BLOCK 包含 break、continue 或 return 語(yǔ)句,__exit__() 方法就會(huì)被調(diào)用,帶三個(gè) None 參數(shù),就跟 BLOCK 正常執(zhí)行完成一樣。(也就是說(shuō),__exit__() 不會(huì)將這些“偽異?!币暈楫惓!#?/p>
如果語(yǔ)法中的"as VAR"部分被省略了,則翻譯中的"VAR ="部分也要被忽略(但 mgr.__enter__() 仍然會(huì)被調(diào)用)。
mgr.__exit__() 的調(diào)用約定如下。如果 finally 子句是通過(guò) BLOCK 的正常完成或通過(guò)非局部 goto(即 BLOCK 中的 break、continue 或 return 語(yǔ)句)到達(dá),則使用三個(gè) None 參數(shù)調(diào)用mgr.__exit__()。如果 finally 子句是通過(guò) BLOCK 引發(fā)的異常到達(dá),則使用異常的類型、值和回溯這三個(gè)參數(shù)調(diào)用 mgr.__exit__()。
重要:如果 mgr.__exit__() 返回“true”,則異常將被“吞滅”。也就是說(shuō),如果返回"true",即便在 with 語(yǔ)句內(nèi)部發(fā)生了異常,也會(huì)繼續(xù)執(zhí)行 with 語(yǔ)句之后的下一條語(yǔ)句。然而,如果 with 語(yǔ)句通過(guò)非局部 goto (break、continue 或 return)跳出,則這個(gè)非局部返回將被重置,不管 mgr.__exit__() 的返回值是什么。這個(gè)細(xì)節(jié)的動(dòng)機(jī)是使 mgr.__exit__() 能夠吞咽異常,而不使異常產(chǎn)生影響(因?yàn)槟J(rèn)的返回值 None為 false,這會(huì)導(dǎo)致異常被重新 raise)。吞下異常的主要用途是使編寫 @contextmanager 裝飾器成為可能,這樣被裝飾的生成器中的 try/except 代碼塊的行為就好像生成器的主體在 with-語(yǔ)句里內(nèi)聯(lián)展開了一樣。
之所以將異常的細(xì)節(jié)傳給__exit__(),而不用 PEP -310 中不帶參數(shù)的__exit__(),原因是考慮到下面例子 3 的 transactional()。該示例會(huì)根據(jù)是否發(fā)生異常,從而決定提交或回滾事務(wù)。我們沒(méi)有用一個(gè) bool 標(biāo)志區(qū)分是否發(fā)生異常,而是傳了完整的異常信息,目的是可以記錄異常日志。依賴于 sys.exc_info() 獲取異常信息的提議被拒絕了;因?yàn)?sys.exc_info() 有著非常復(fù)雜的語(yǔ)義,它返回的異常信息完全有可能是很久之前就捕獲的。有人還提議添加一個(gè)布爾值,用于區(qū)分是到達(dá) BLOCK 結(jié)尾,還是非局部 goto。這因?yàn)檫^(guò)于復(fù)雜和不必要而被拒絕;對(duì)于數(shù)據(jù)庫(kù)事務(wù)回滾,非局部 goto 應(yīng)該被認(rèn)為是正常的。
為了促進(jìn) Python 代碼中上下文的鏈接作用,__exit__() 方法不應(yīng)該繼續(xù) raise 傳遞給它的錯(cuò)誤。在這種情況下,__exit__() 方法的調(diào)用者應(yīng)該負(fù)責(zé)處理 raise。
這樣,如果調(diào)用者想知道__exit__() 是否調(diào)用失?。ǘ皇窃趥鞒鲈煎e(cuò)誤之前就完成清理),它就可以自己判斷。
如果__exit__() 沒(méi)有返回錯(cuò)誤,那么就可以將__exit__() 方法本身解釋為成功(不管原始錯(cuò)誤是被傳播還是抑制)。
然而,如果__exit__() 向其調(diào)用者傳播了異常,這就意味著__exit__() 本身已經(jīng)失敗。因此,__exit__() 方法應(yīng)該避免引發(fā)錯(cuò)誤,除非它們確實(shí)失敗了。(允許原始錯(cuò)誤繼續(xù)并不是失敗。)
過(guò)渡計(jì)劃
在 Python 2.5 中,新語(yǔ)法需要通過(guò) future 引入:
from __future__ import with_statement
它會(huì)引入'with'和'as'關(guān)鍵字。如果沒(méi)有導(dǎo)入,使用'with'或'as'作為標(biāo)識(shí)符時(shí),將導(dǎo)致報(bào)錯(cuò)。
在 Python 2.6 中,新語(yǔ)法總是生效的,'with'和'as'已經(jīng)是關(guān)鍵字。
生成器裝飾器
隨著 PEP-342 被采納,我們可以編寫一個(gè)裝飾器,令其使用只 yield 一次的生成器來(lái)控制 with 語(yǔ)句。這是一個(gè)裝飾器的粗略示例:
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
這個(gè)裝飾器可以這樣使用:
@contextmanager
def opening(filename):
f = open(filename) # IOError is untouched by GeneratorContext
try:
yield f
finally:
f.close() # Ditto for errors here (however unlikely)
這個(gè)裝飾器的健壯版本將會(huì)加入到標(biāo)準(zhǔn)庫(kù)中。
標(biāo)準(zhǔn)庫(kù)中的上下文管理器
可以將__enter__() 和__exit__() 方法賦予某些對(duì)象,如文件、套接字和鎖,這樣就不用寫:
with locking(myLock):
BLOCK
而是簡(jiǎn)單地寫成:
with myLock:
BLOCK
我想我們應(yīng)該謹(jǐn)慎對(duì)待它;它可能會(huì)導(dǎo)致以下的錯(cuò)誤:
f = open(filename)
with f:
BLOCK1
with f:
BLOCK2
它可能跟你想的不一樣(在進(jìn)入 block2 之前,f 已經(jīng)關(guān)閉了)。
另一方面,這樣的錯(cuò)誤很容易診斷;例如,當(dāng)?shù)诙€(gè) with 語(yǔ)句再調(diào)用 f.__enter__() 時(shí),上面的生成器裝飾器將引發(fā) RuntimeError。如果在一個(gè)已關(guān)閉的文件對(duì)象上調(diào)用__enter__,則可能引發(fā)類似的錯(cuò)誤。
在 Python 2.5中,以下類型被標(biāo)識(shí)為上下文管理器:
- file
- thread.LockType
- threading.Lock
- threading.RLock
- threading.Condition
- threading.Semaphore
- threading.BoundedSemaphore
還將在 decimal 模塊添加一個(gè)上下文管理器,以支持在 with 語(yǔ)句中使用本地的十進(jìn)制算術(shù)上下文,并在退出 with 語(yǔ)句時(shí),自動(dòng)恢復(fù)原始上下文。
標(biāo)準(zhǔn)術(shù)語(yǔ)
本 PEP 提議將由__enter__() 和 __exit__() 方法組成的協(xié)議稱為“上下文管理器協(xié)議”,并將實(shí)現(xiàn)該協(xié)議的對(duì)象稱為“上下文管理器”。[4]
緊跟著 with 關(guān)鍵字的表達(dá)式被稱為“上下文表達(dá)式”,該表達(dá)式提供了上下文管理器在with 代碼塊中所建立的運(yùn)行時(shí)環(huán)境的主要線索。
目前為止, with 語(yǔ)句體中的代碼和 as 關(guān)鍵字后面的變量名(一個(gè)或多個(gè))還沒(méi)有特殊的術(shù)語(yǔ)??梢允褂靡话愕男g(shù)語(yǔ)“語(yǔ)句體”和“目標(biāo)列表”,如果這些術(shù)語(yǔ)不清晰,可以使用“with”或“with statement”作為前綴。
考慮到可能存在 decimal 模塊的算術(shù)上下文這樣的對(duì)象,因此術(shù)語(yǔ)“上下文”是有歧義的。如果想要更加具體的話,可以使用術(shù)語(yǔ)“上下文管理器”,表示上下文表達(dá)式所創(chuàng)建的具體對(duì)象;使用術(shù)語(yǔ)“運(yùn)行時(shí)上下文”或者(最好是)"運(yùn)行時(shí)環(huán)境",表示上下文管理器所做出的實(shí)際狀態(tài)的變更。當(dāng)簡(jiǎn)單地討論 with 語(yǔ)句的用法時(shí),歧義性無(wú)關(guān)緊要,因?yàn)樯舷挛谋磉_(dá)式完全定義了對(duì)運(yùn)行時(shí)環(huán)境所做的更改。當(dāng)討論 with 語(yǔ)句本身的機(jī)制以及如何實(shí)際實(shí)現(xiàn)上下文管理器時(shí),這些術(shù)語(yǔ)的區(qū)別才是重要的。
緩存上下文管理器
許多上下文管理器(例如文件和基于生成器的上下文)都是一次性的對(duì)象。一旦__exit__() 方法被調(diào)用,上下文管理器將不再可用(例如:文件已經(jīng)被關(guān)閉,或者底層生成器已經(jīng)完成執(zhí)行)。
對(duì)于多線程代碼,以及嵌套的 with 語(yǔ)句想要使用同一個(gè)上下文管理器,最簡(jiǎn)單的方法是給每個(gè) with 語(yǔ)句一個(gè)新的管理器對(duì)象。并非巧合的是,標(biāo)準(zhǔn)庫(kù)中所有支持重用的上下文管理器都來(lái)自 threading 模塊——它們都被設(shè)計(jì)用來(lái)處理由線程和嵌套使用所產(chǎn)生的問(wèn)題。
這意味著,為了保存帶有特定初始化參數(shù)(為了用在多個(gè) with 語(yǔ)句)的上下文管理器,通常需要將它存儲(chǔ)在一個(gè)無(wú)參數(shù)的可調(diào)用對(duì)象,然后在每個(gè)語(yǔ)句的上下文表達(dá)式中調(diào)用,而不是直接把上下文管理器緩存起來(lái)。
如果此限制不適用,在受影響的上下文管理器的文檔中,應(yīng)該清楚地指出這一點(diǎn)。
解決的問(wèn)題
以下的問(wèn)題經(jīng)由 BDFL 的裁決而解決(并且在 python-dev 上沒(méi)有重大的反對(duì)意見)。
1、當(dāng)?shù)讓拥纳善?迭代器行為異常時(shí),GeneratorContextManager 應(yīng)該引發(fā)什么異常?下面引用的內(nèi)容是 Guido 為本 PEP及 PEP-342 (見[8])中生成器的 close() 方法選擇 RuntimeError 的原因:“我不愿意只是為了它而引入一個(gè)新的異常類,因?yàn)檫@不是我想讓人們捕獲的異常:我想讓它變成一個(gè)回溯(traceback),被程序員看到并且修復(fù)。因此,我認(rèn)為它們都應(yīng)該引發(fā) RuntimeError。有一些引發(fā) RuntimeError 的先例:Python 核心代碼在檢測(cè)到無(wú)限遞歸時(shí),遇到未初始化的對(duì)象時(shí)(以及其它各種各樣的情況)?!?/p>
2、如果在with語(yǔ)句所涉及的類中沒(méi)有相關(guān)的方法,則最好是拋出AttributeError而不是TypeError。抽象對(duì)象C API引發(fā)TypeError而不是AttributeError,這只是歷史的一個(gè)偶然,而不是經(jīng)過(guò)深思熟慮的設(shè)計(jì)決策[11]。
3、帶有__enter__ /__exit__方法的對(duì)象被稱為“上下文管理器”,將生成器函數(shù)轉(zhuǎn)化為上下文管理器工廠的是 contextlib.contextmanager 裝飾器。在 2.5版本發(fā)布期間,有人提議使用其它的叫法[16],但沒(méi)有足夠令人信服的理由。
拒絕的選項(xiàng)
在長(zhǎng)達(dá)幾個(gè)月的時(shí)間里,對(duì)于是否要抑制異常(從而避免隱藏的流程控制),出現(xiàn)了一場(chǎng)令人痛苦的拉鋸戰(zhàn),最終,Guido 決定要抑制異常[13]。
本 PEP 的另一個(gè)話題也引起了無(wú)休止的爭(zhēng)論,即是否要提供一個(gè)__context__() 方法,類似于可迭代對(duì)象的__iter__() 方法[5][7][9]。源源不斷的問(wèn)題[10][13]在解釋它是什么、為什么是那樣、以及它是如何工作的,最終導(dǎo)致 Guido 完全拋棄了這個(gè)東西[15](這很讓人歡欣鼓舞?。?/p>
還有人提議直接使用 PEP-342 的生成器 API 來(lái)定義 with 語(yǔ)句[6],但這很快就不予考慮了,因?yàn)樗鼤?huì)導(dǎo)致難以編寫不基于生成器的上下文管理器。
例子
基于生成器的示例依賴于 PEP-342。另外,有些例子是不實(shí)用的,因?yàn)闃?biāo)準(zhǔn)庫(kù)中有現(xiàn)成的對(duì)象可以在 with 語(yǔ)句中直接使用,例如 threading.RLock。
例子中那些函數(shù)名所用的時(shí)態(tài)并不是隨意的。過(guò)去時(shí)態(tài)(“-ed”)的函數(shù)指的是在__enter__方法中執(zhí)行,并在__exit__方法中反執(zhí)行的動(dòng)作。進(jìn)行時(shí)態(tài)("-ing")的函數(shù)指的是準(zhǔn)備在__exit__方法中執(zhí)行的動(dòng)作。
1、一個(gè)鎖的模板,在開始時(shí)獲取,在離開時(shí)釋放:
@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、一個(gè)打開文件的模板,確保當(dāng)代碼被執(zhí)行后,文件會(huì)被關(guān)閉:
@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、一個(gè)數(shù)據(jù)庫(kù)事務(wù)的模板,用于提交或回滾:
@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()
(這個(gè)例子很容易被修改來(lái)實(shí)現(xiàn)其他相對(duì)無(wú)狀態(tài)的例子;這表明,如果不需要保留特殊的狀態(tài),就不必要使用生成器。)
5、臨時(shí)重定向 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"
當(dāng)然,這不是線程安全的,但是若不用管理器的話,本身也不是線程安全的。在單線程程序(例如腳本)中,這種做法很受歡迎。
6、opened() 的一個(gè)變體,也返回一個(gè)錯(cuò)誤條件:
@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、另一個(gè)有用的操作是阻塞信號(hào)。它的用法是這樣的:
import signal
with signal.blocked():
# code executed without worrying about signals
它的參數(shù)是可選的,表示要阻塞的信號(hào)列表;在默認(rèn)情況下,所有信號(hào)都被阻塞。具體實(shí)現(xiàn)就留給讀者作為練習(xí)吧。
8、此特性還有一個(gè)用途是 Decimal 上下文。下面是 Michael Chermside 發(fā)布的一個(gè)簡(jiǎn)單的例子:
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 庫(kù)參考文檔):
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 模塊的一個(gè)簡(jiǎn)單的上下文管理器:
@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、一個(gè)通用的“對(duì)象關(guān)閉”上下文管理器:
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()
這可以確保關(guān)閉任何帶有 close 方法的東西,無(wú)論是文件、生成器,還是其他東西。它甚至可以在對(duì)象并不需要關(guān)閉的情況下使用(例如,一個(gè)接受了任意可迭代對(duì)象的函數(shù)):
# 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 模塊包含了這個(gè)上下文管理器的一個(gè)版本)
11、PEP-319 給出了一個(gè)用例,它也有一個(gè) release() 上下文,能臨時(shí)釋放先前獲得的鎖;這個(gè)用例跟前文的例子 4 很相似,只是交換了 acquire() 和 release() 的調(diào)用:
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、一個(gè)“嵌套型”上下文管理器,自動(dòng)從左到右嵌套所提供的上下文,可以避免過(guò)度縮進(jìn):
@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
等價(jià)于:
with a as x:
with b as y:
with c as z:
# Perform operation
(Python 2.5 的 contextlib 模塊包含了這個(gè)上下文管理器的一個(gè)版本)
參考實(shí)現(xiàn)
在 2005 年 6 月 27 日的 EuroPython 會(huì)議上,Guido 首次采納了這個(gè) PEP。之后它添加了__context__方法,并被再次采納。此 PEP 在 Python 2.5 a1 子版本中實(shí)現(xiàn),__context__() 方法在 Python 2.5b1 中被刪除。
致謝
許多人對(duì)這個(gè) PEP 中的想法和概念作出了貢獻(xiàn),包括在 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
版權(quán)
本文檔已進(jìn)入公共領(lǐng)域。
