python上下文管理器(context manager)

首先,什么是上下文管理器或衡?上下文管理器就是實(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ò)程就很清楚了。

  1. 執(zhí)行上下文表達(dá)式蕊唐,獲取上下文管理器
  2. 加載上下文管理器的__exit__()方法以備后期調(diào)用
  3. 調(diào)用上下文管理器的__enter__()方法
  4. 如果with語(yǔ)句有指定目標(biāo)變量屋摔,將從__enter__()方法獲取的相關(guān)對(duì)象賦值給目標(biāo)變量
  5. 執(zhí)行with語(yǔ)句體
  6. 調(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末匈仗,一起剝皮案震驚了整個(gè)濱河市瓢剿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌悠轩,老刑警劉巖间狂,帶你破解...
    沈念sama閱讀 212,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異火架,居然都是意外死亡鉴象,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)何鸡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)纺弊,“玉大人,你說(shuō)我怎么就攤上這事骡男∠危” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,369評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵隔盛,是天一觀的道長(zhǎng)犹菱。 經(jīng)常有香客問(wèn)我,道長(zhǎng)吮炕,這世上最難降的妖魔是什么腊脱? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,799評(píng)論 1 285
  • 正文 為了忘掉前任,我火速辦了婚禮龙亲,結(jié)果婚禮上陕凹,老公的妹妹穿的比我還像新娘。我一直安慰自己鳄炉,他們只是感情好杜耙,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,910評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著迎膜,像睡著了一般泥技。 火紅的嫁衣襯著肌膚如雪浆兰。 梳的紋絲不亂的頭發(fā)上磕仅,一...
    開(kāi)封第一講書(shū)人閱讀 50,096評(píng)論 1 291
  • 那天珊豹,我揣著相機(jī)與錄音,去河邊找鬼榕订。 笑死店茶,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的劫恒。 我是一名探鬼主播贩幻,決...
    沈念sama閱讀 39,159評(píng)論 3 411
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼两嘴!你這毒婦竟也來(lái)了丛楚?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,917評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤憔辫,失蹤者是張志新(化名)和其女友劉穎趣些,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體贰您,經(jīng)...
    沈念sama閱讀 44,360評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡坏平,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,673評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了锦亦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片舶替。...
    茶點(diǎn)故事閱讀 38,814評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖杠园,靈堂內(nèi)的尸體忽然破棺而出顾瞪,到底是詐尸還是另有隱情,我是刑警寧澤抛蚁,帶...
    沈念sama閱讀 34,509評(píng)論 4 334
  • 正文 年R本政府宣布玲昧,位于F島的核電站,受9級(jí)特大地震影響篮绿,放射性物質(zhì)發(fā)生泄漏孵延。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,156評(píng)論 3 317
  • 文/蒙蒙 一亲配、第九天 我趴在偏房一處隱蔽的房頂上張望尘应。 院中可真熱鬧,春花似錦吼虎、人聲如沸犬钢。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)玷犹。三九已至,卻和暖如春洒疚,著一層夾襖步出監(jiān)牢的瞬間歹颓,已是汗流浹背坯屿。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,123評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留巍扛,地道東北人领跛。 一個(gè)月前我還...
    沈念sama閱讀 46,641評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像撤奸,于是被迫代替她去往敵國(guó)和親吠昭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,728評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容