首先,什么是上下文管理器或衡?上下文管理器就是實(shí)現(xiàn)了上下文管理協(xié)議的對(duì)象焦影。主要用于保存和恢復(fù)各種全局狀態(tài),關(guān)閉文件等封断,上下文管理器本身就是一種裝飾器斯辰。
下面我們就通過(guò)一下幾個(gè)部分,來(lái)介紹下上下文管理器坡疼。
上下文管理協(xié)議(context management protocol)
上下文管理協(xié)議包括兩個(gè)方法:
contextmanager.__enter__()
從該方法進(jìn)入運(yùn)行時(shí)上下文彬呻,并返回當(dāng)前對(duì)象或者與運(yùn)行時(shí)上下文相關(guān)的其他對(duì)象。如果with語(yǔ)句有as關(guān)鍵詞存在柄瑰,返回值會(huì)綁定在as后的變量上闸氮。contextmanager.__exit__(exc_type, exc_val, exc_tb)
退出運(yùn)行時(shí)上下文,并返回一個(gè)布爾值標(biāo)示是否有需要處理的異常教沾。如果在執(zhí)行with語(yǔ)句體時(shí)發(fā)生異常蒲跨,那退出時(shí)參數(shù)會(huì)包括異常類型、異常值详囤、異常追蹤信息财骨,否則镐作,3個(gè)參數(shù)都是None藏姐。
我們看一個(gè)常見(jiàn)的文件打開(kāi)操作
>>> with open("/etc/hosts", "r") as file:
... dir(file)
...
['__class__', '__delattr__', '__doc__', '__enter__', '__exit__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'closed', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'mode', 'name', 'newlines', 'next', 'read', 'readinto', 'readline', 'readlines', 'seek', 'softspace', 'tell', 'truncate', 'write', 'writelines', 'xreadlines']
open函數(shù)會(huì)返回一個(gè)文件類型變量,這個(gè)文件類實(shí)現(xiàn)了上下文管理協(xié)議该贾。
with語(yǔ)句
說(shuō)上下文管理器就不得不說(shuō)with語(yǔ)句羔杨,為什么呢?
因?yàn)閣ith語(yǔ)句就是為支持上下文管理器而存在的杨蛋,使用上下文管理協(xié)議的方法包裹一個(gè)代碼塊(with語(yǔ)句體)的執(zhí)行兜材,并為try...except...finally提供了一個(gè)方便使用的封裝理澎。
with語(yǔ)句的語(yǔ)法如下:
with EXPR as VAR:
BLOCK
with和as是關(guān)鍵詞,EXPR就是上下文表達(dá)式曙寡,是任意表達(dá)式(一個(gè)表達(dá)式糠爬,不是表達(dá)式列表),VAR是賦值的目標(biāo)變量举庶。"as VAR"是可選的执隧。
上述語(yǔ)句的底層實(shí)現(xiàn)可以這樣描述:
mgr = (EXPR)
exit = type(mgr).__exit__ # 并沒(méi)有調(diào)用
value = type(mgr).__enter__(mgr)
exc = True
try:
try:
VAR = value # 如果有"as VAR"
BLOCK
except:
# 這里會(huì)處理異常
exc = False
if not exit(mgr, *sys.exc_info()):
raise
# 如果__exit__返回值是false,異常將被傳播户侥;如果返回值是真镀琉,異常將被終止
finally:
if exc:
exit(mgr, None, None, None)
這樣with語(yǔ)句的執(zhí)行過(guò)程就很清楚了。
- 執(zhí)行上下文表達(dá)式蕊唐,獲取上下文管理器
- 加載上下文管理器的
__exit__()
方法以備后期調(diào)用 - 調(diào)用上下文管理器的
__enter__()
方法 - 如果with語(yǔ)句有指定目標(biāo)變量屋摔,將從
__enter__()
方法獲取的相關(guān)對(duì)象賦值給目標(biāo)變量 - 執(zhí)行with語(yǔ)句體
- 調(diào)用上下文管理器的
__exit__()
方法,如果是with語(yǔ)句體造成的異常退出替梨,那異常類型钓试、異常值、異常追蹤信息將被傳給__exit__()
副瀑,否則亚侠,3個(gè)參數(shù)都是None。
也可以將多個(gè)表達(dá)式組織在一起俗扇。
with A() as a, B() as b:
BLOCK
它等價(jià)于
with A() as a:
with B() as b:
BLOCK
注:多上下文表達(dá)式是從python 2.7開(kāi)始支持的
自定義上下文管理器
class ContextManager(object):
def __init__(self):
print '__init__()'
def __enter__(self):
print '__enter__()'
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print "__exit__()"
with ContextManager():
print "OK, we can do something here~~"
#輸出
__init__()
__enter__()
OK, we can do something here~~
__exit__()
這個(gè)例子里硝烂,with語(yǔ)句沒(méi)有使用as關(guān)鍵詞,返回了當(dāng)前對(duì)象铜幽。下面我們?cè)倏匆粋€(gè)返回非當(dāng)前對(duì)象的例子
class InnerContext(object):
def __init__(self, obj):
print 'InnerContext.__init__(%s)' % obj
def do_something(self):
print 'InnerContext.do_something()'
def __del__(self):
print 'InnerContext.__del__()'
class ContextManager(object):
def __init__(self):
print 'ContextManager.__init__()'
def __enter__(self):
print 'ContextManager.__enter__()'
return InnerContext(self)
def __exit__(self, exc_type, exc_val, exc_tb):
print "ContextManager.__exit__()"
with ContextManager() as obj:
obj.do_something()
print "OK, we can do something here~~"
#輸出
ContextManager.__init__()
ContextManager.__enter__()
InnerContext.__init__(<__main__.ContextManager object at 0x1012f95d0>)
InnerContext.do_something()
OK, we can do something here~~
ContextManager.__exit__()
InnerContext.__del__()
下面再來(lái)看下異常處理的例子
class ContextManager(object):
def __init__(self, flag):
print 'ContextManager.__init__(%s)' % flag
self.flag = flag
def __enter__(self):
print 'ContextManager.__enter__()'
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print 'ContextManager.__exit__(%s, %s, %s)' % (exc_type, exc_val, exc_tb)
return self.flag
with ContextManager(True):
raise RuntimeError('error message handled')
print
with ContextManager(False):
raise RuntimeError('error message propagated')
#輸出
ContextManager.__init__(True)
ContextManager.__enter__()
ContextManager.__exit__(<type 'exceptions.RuntimeError'>, error message handled, <traceback object at 0x10d69dbd8>)
ContextManager.__init__(False)
ContextManager.__enter__()
ContextManager.__exit__(<type 'exceptions.RuntimeError'>, error message propagated, <traceback object at 0x109e0fbd8>)
Traceback (most recent call last):
File "ContextManager.py", line 19, in <module>
raise RuntimeError('error message propagated')
RuntimeError: error message propagated
contextlib 模塊
對(duì)于上下文的管理滞谢,python也提供了內(nèi)建的模塊contextlib來(lái)實(shí)現(xiàn)相同的機(jī)制,而且這種通過(guò)生成器和裝飾器實(shí)現(xiàn)的上下文管理器除抛,看起來(lái)比with語(yǔ)句和手動(dòng)實(shí)現(xiàn)上下文管理協(xié)議更優(yōu)雅狮杨。contextlib提供了3個(gè)相關(guān)函數(shù),分別用于不同的場(chǎng)景到忽。
contextlib.contextmanager(func)
手動(dòng)實(shí)現(xiàn)上下文管理協(xié)議并不難橄教,但我們更常遇到的情況是管理非常簡(jiǎn)單的上下文,這時(shí)就可以使用contextmanager將一個(gè)生成器轉(zhuǎn)化成上下文管理器喘漏,而不需要再去實(shí)現(xiàn)上下文管理協(xié)議护蝶。
import contextlib
@contextlib.contextmanager
def createContextManager(name):
print '__enter__ %s' % name
yield name
print '__exit__ %s' % name
with createContextManager('foo') as value:
print value
#輸出
__enter__ foo
foo
__exit__ foo
contextlib.nested(mgr1[, mgr2[, ...]])
有時(shí)我們會(huì)有同時(shí)管理多個(gè)上下文的需求,這時(shí)候就可以使用nested,該函數(shù)可以將多個(gè)上下文管理器結(jié)合為一個(gè)嵌套的上下管理器。
with contextlib.nested(createContextManager('a'),createContextManager('b'),createContextManager('c')) as (a, b, c):
print a, b, c
#輸出
__enter__ a
__enter__ b
__enter__ c
a b c
__exit__ c
__exit__ b
__exit__ a
等價(jià)于
with createContextManager('a') as a:
with createContextManager('b') as b:
with createContextManager('c') as c:
print a, b, c
#輸出
__enter__ a
__enter__ b
__enter__ c
a b c
__exit__ c
__exit__ b
__exit__ a
python 2.7 以后翩迈,with已經(jīng)支持多個(gè)上下文直接嵌套操作持灰,所以nested已經(jīng)不建議使用了。
with createContextManager('a') as a,createContextManager('b') as b,createContextManager('c') as c:
print a, b, c
#輸出
__enter__ a
__enter__ b
__enter__ c
a b c
__exit__ c
__exit__ b
__exit__ a
contextlib.closing(thing)
從本文的第一個(gè)例子里可以看出负饲,文件類是支持上下文管理協(xié)議的堤魁,可以直接用with語(yǔ)句喂链,還有一些對(duì)象并不支持該協(xié)議,但使用的時(shí)候又要確保正常退出妥泉,這時(shí)就可以使用closing創(chuàng)建上下文管理器椭微。它等價(jià)于
from contextlib import contextmanager
@contextmanager
def closing(thing):
try:
yield thing
finally:
thing.close()
但使用closing類似如下:
from contextlib import closing
import urllib
with closing(urllib.urlopen('http://www.python.org')) as page:
for line in page:
print line
你不必顯示的關(guān)閉page, 即使拋出異常也會(huì)page也會(huì)正常關(guān)閉。
注: with語(yǔ)句體(with statement body) 在with語(yǔ)句中盲链,with包裹起來(lái)的語(yǔ)句體并沒(méi)有一個(gè)非常正式的名稱赏表,只是一般大家都這么叫。
reference
- https://www.python.org/dev/peps/pep-0343/
- https://docs.python.org/2.7/library/stdtypes.html#typecontextmanager
- https://docs.python.org/2.7/reference/compound_stmts.html#with
- https://docs.python.org/2.7/library/contextlib.html?highlight=contextlib#contextlib.contextmanager
- https://pymotw.com/2/contextlib/