【Python魔術(shù)方法】上下文管理器(__enter__和__exit__)

本文環(huán)境

win7_64 + Python 2.7.10

文件操作

一般文件操作可以通過(guò)open的方式獲取一個(gè)文件對(duì)象fp苟弛,比如:

fp = open('test.txt', 'r')

然后我們可以通過(guò)fp對(duì)文件進(jìn)行讀操作,操作完成后,我們需要通過(guò)調(diào)用fp對(duì)象的close方法顯示的關(guān)閉資源,以便讓python可以正確的釋放相關(guān)系統(tǒng)資源

fp.close()

如果在close之前出現(xiàn)了異常,導(dǎo)致代碼不能正常走到close,就會(huì)出現(xiàn)資源不能正常釋放的情況

fp = open('test.txt', 'r')
a = 1 / 0 # 這里引發(fā)了異常同廉,下面的代碼無(wú)法執(zhí)行
fp.close()

一般可以通過(guò)try...except...finally來(lái)捕獲異常,因?yàn)閒inally里面的代碼是一定會(huì)走到柑司,故可以把資源釋放的動(dòng)作放到這里

fp = open('test.txt', 'r')
try:
    do_something()
    a = 1/ 0
except:
    do_exc()
finally:
    fp.close() # 能正確釋放

上下文管理器

上面的代碼雖然可以滿足我們的要求迫肖,但是比較冗長(zhǎng),而且也不符合python提倡的代碼簡(jiǎn)潔性攒驰,所以有一個(gè)概念就是上下文管理器蟆湖,類比到代碼就是with語(yǔ)句

with open('test.txt', 'r') as fp:
    do_something()

上面的代碼,就可以自動(dòng)的在執(zhí)行完do_something()這段邏輯后正確的進(jìn)行釋放玻粪,會(huì)自動(dòng)的調(diào)用close

實(shí)現(xiàn)自己的上下文管理器

可以通過(guò)定義一個(gè)類隅津,滿足上下文管理器協(xié)議的要求定義,就可以用with語(yǔ)句來(lái)調(diào)用劲室,類里面需要定義__enter____exit__兩個(gè)方法即可

class MyContext:
    def __init__(self):
        print('__init__')
        
    def func(self):
        print('func')
        
    def __enter__(self):
        print('__enter__')
        
    def __exit__(self, exc_type, exc_value, traceback):
        print('__exit__')
        
def test():
    print('test')
        
with MyContext():
    test()

執(zhí)行后結(jié)果:

__init__
__enter__
test
__exit__

執(zhí)行順序很明顯
假如我們?cè)?code>test里面出現(xiàn)了異常伦仍,在看看會(huì)執(zhí)行哪些方法

class MyContext:
    def __init__(self):
        print('__init__')
        
    def func(self):
        print('func')
        
    def __enter__(self):
        print('__enter__')
        
    def __exit__(self, exc_type, exc_value, traceback):
        print('__exit__')
        
def test():
    print('test')
    return 1 / 0 #這里引發(fā)異常
        
with MyContext():
    test()

結(jié)果

__init__
__enter__
test
__exit__
Traceback (most recent call last):
  File "test.py", line 19, in <module>
    test()
  File "test.py", line 16, in test
    return 1 / 0
ZeroDivisionError: integer division or modulo by zero

同樣,所有的都執(zhí)行了很洋,就算test引發(fā)了異常充蓝,但是__exit__仍然會(huì)執(zhí)行
上面都是用的with語(yǔ)句,現(xiàn)在來(lái)看看with...as

class MyContext:
    def __init__(self):
        print('__init__')
        
    def func(self):
        print('func')
        
    def __enter__(self):
        print('__enter__')
        
    def __exit__(self, exc_type, exc_value, traceback):
        print('__exit__')
        
# 這個(gè)with語(yǔ)句不等同于 my = MyContext(),而是等同于my = MyContext().__enter__()谓苟,由于__enter__并
# 沒(méi)有返回任何值官脓,默認(rèn)就是None,故在調(diào)用my.fun()會(huì)拋出異常
with MyContext() as my:
    my.func()

結(jié)果

__init__
__enter__
__exit__
Traceback (most recent call last):
  File "test.py", line 15, in <module>
    my.func()
AttributeError: 'NoneType' object has no attribute 'func'

我們把__enter__返回self自己

class MyContext:
    def __init__(self):
        print('__init__')
        
    def func(self):
        print('func')
        
    def __enter__(self):
        print('__enter__')
        return self
        
    def __exit__(self, exc_type, exc_value, traceback):
        print('__exit__')
        
with MyContext() as my:
    my.func()

結(jié)果就可以正常執(zhí)行

__init__
__enter__
func
__exit__

執(zhí)行順序和異常拋出問(wèn)題

  1. __enter__返回之前出現(xiàn)異常娜谊,那么上下文管理器是沒(méi)有形成的,此時(shí)__exit__是不能被執(zhí)行的斤讥,除非顯示調(diào)用
    首先看__init__里面引發(fā)異常
class MyContext:
    def __init__(self):
        print('__init__')
        1 / 0
        
    def func(self):
        print('func')
        
    def __enter__(self):
        print('__enter__')
        return self
        
    def __exit__(self, exc_type, exc_value, traceback):
        print('__exit__')
        
with MyContext() as my:
    my.func()

結(jié)果只執(zhí)行到了__init__纱皆,引發(fā)異常后,代碼終止芭商,連__enter__都無(wú)法執(zhí)行

__init__
Traceback (most recent call last):
  File "test.py", line 16, in <module>
    with MyContext() as my:
  File "test.py", line 4, in __init__
    1 / 0
ZeroDivisionError: integer division or modulo by zero

在來(lái)看看__enter__自己出現(xiàn)異常

class MyContext:
    def __init__(self):
        print('__init__')
        
    def func(self):
        print('func')
        
    def __enter__(self):
        print('__enter__')
        1 / 0
        return self
        
    def __exit__(self, exc_type, exc_value, traceback):
        print('__exit__')
        
with MyContext() as my:
    my.func()

結(jié)果是

__init__
__enter__
Traceback (most recent call last):
  File "test.py", line 16, in <module>
    with MyContext() as my:
  File "test.py", line 10, in __enter__
    1 / 0
ZeroDivisionError: integer division or modulo by zero

雖然執(zhí)行到了__enter__派草,但是仍然沒(méi)有執(zhí)行__exit__,代碼就終止了

  1. __exit__的三個(gè)參數(shù)對(duì)應(yīng)了引發(fā)異常后的三個(gè)參數(shù)铛楣,對(duì)應(yīng)的就是sys.exc_info()返回的對(duì)象近迁,如果沒(méi)有引發(fā)異常,它們的值都是None
    首先看下出現(xiàn)異常它們的值
class MyContext:
    def __init__(self):
        print('__init__')
        
    def func(self):
        1 / 0
        print('func')
        
    def __enter__(self):
        print('__enter__')
        return self
        
    def __exit__(self, exc_type, exc_value, traceback):
        print('__exit__')
        print(exc_type, exc_value, traceback)
        
with MyContext() as my:
    my.func()

結(jié)果是

__init__
__enter__
__exit__
# 分別是異常類型簸州,異常值和異常跟蹤對(duì)象
(<type 'exceptions.ZeroDivisionError'>, ZeroDivisionError('integer division or modulo by zero',), <traceback object at 0x000000000269EC08>)
Traceback (most recent call last):
  File "test.py", line 18, in <module>
    my.func()
  File "test.py", line 6, in func
    1 / 0
ZeroDivisionError: integer division or modulo by zero

再來(lái)看看未出現(xiàn)異常的時(shí)候

class MyContext:
    def __init__(self):
        print('__init__')
        
    def func(self):
        print('func')
        
    def __enter__(self):
        print('__enter__')
        return self
        
    def __exit__(self, exc_type, exc_value, traceback):
        print('__exit__')
        print(exc_type, exc_value, traceback)
        
with MyContext() as my:
    my.func()

結(jié)果是

__init__
__enter__
func
__exit__
# 三個(gè)值都是None
(None, None, None)
  1. __exit__默認(rèn)是返回的None鉴竭,上面的例子可以看出,異常雖然出現(xiàn)了岸浑,但是仍然會(huì)繼續(xù)拋出搏存,我們可以通過(guò)讓__exit__返回True來(lái)屏蔽異常
class MyContext:
    def __init__(self):
        print('__init__')
        
    def func(self):
        1 / 0
        print('func')
        
    def __enter__(self):
        print('__enter__')
        return self
        
    def __exit__(self, exc_type, exc_value, traceback):
        print('__exit__')
        print(exc_type, exc_value, traceback)
        return True
        
with MyContext() as my:
    my.func()

結(jié)果是

__init__
__enter__
__exit__
(<type 'exceptions.ZeroDivisionError'>, ZeroDivisionError('integer division or modulo by zero',), <traceback object at 0x000000000267EC08>)

由于__exit__返回了True,異常被屏蔽矢洲,代碼不會(huì)終止璧眠,會(huì)繼續(xù)往下執(zhí)行,但是注意读虏,屏蔽異常责静,一定要自己在__exit__里面處理異常,或者你根本不care這個(gè)異常

總結(jié)

  1. __enter__需要返回一個(gè)對(duì)象盖桥,才能通過(guò)with...as把一個(gè)變量引用到這個(gè)對(duì)象上
  2. __exit__有4個(gè)參數(shù)灾螃,后面三個(gè)分別是exc_type, exc_valuetraceback
  3. __enter__必須正確返回后(就算是返回默認(rèn)的None),上下文管理器協(xié)議才生效
  4. __exit__默認(rèn)返回None揩徊,出現(xiàn)異常仍然會(huì)繼續(xù)拋出睦焕,返回True則會(huì)屏蔽異常

參考

https://docs.python.org/2/reference/datamodel.html#with-statement-context-managers
https://docs.python.org/2/reference/compound_stmts.html#the-with-statement

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市靴拱,隨后出現(xiàn)的幾起案子垃喊,更是在濱河造成了極大的恐慌,老刑警劉巖袜炕,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件本谜,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡偎窘,警方通過(guò)查閱死者的電腦和手機(jī)乌助,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門溜在,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人他托,你說(shuō)我怎么就攤上這事掖肋。” “怎么了赏参?”我有些...
    開封第一講書人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵志笼,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我把篓,道長(zhǎng)纫溃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任韧掩,我火速辦了婚禮紊浩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘疗锐。我一直安慰自己坊谁,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開白布滑臊。 她就那樣靜靜地躺著呜袁,像睡著了一般。 火紅的嫁衣襯著肌膚如雪简珠。 梳的紋絲不亂的頭發(fā)上阶界,一...
    開封第一講書人閱讀 51,287評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音聋庵,去河邊找鬼膘融。 笑死,一個(gè)胖子當(dāng)著我的面吹牛祭玉,可吹牛的內(nèi)容都是我干的氧映。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼脱货,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼岛都!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起振峻,我...
    開封第一講書人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤臼疫,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后扣孟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體烫堤,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鸽斟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拔创。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖富蓄,靈堂內(nèi)的尸體忽然破棺而出剩燥,到底是詐尸還是另有隱情,我是刑警寧澤立倍,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布灭红,位于F島的核電站,受9級(jí)特大地震影響帐萎,放射性物質(zhì)發(fā)生泄漏比伏。R本人自食惡果不足惜胜卤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一疆导、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧葛躏,春花似錦澈段、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至摩窃,卻和暖如春兽叮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背猾愿。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工鹦聪, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蒂秘。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓泽本,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親姻僧。 傳聞我的和親對(duì)象是個(gè)殘疾皇子规丽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354

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