Python 的 with 語句

引言

with 語句是從 Python 2.5 開始引入的一種與異常處理相關(guān)的功能(2.5 版本中要通過 from __future__ import with_statement 導(dǎo)入后才可以使用),從 2.6 版本開始缺省可用(參考 What's new in Python 2.6? 中 with 語句相關(guān)部分介紹)俏扩。with 語句適用于對資源進行訪問的場合,確保不管使用過程中是否發(fā)生異常都會執(zhí)行必要的“清理”操作粪摘,釋放資源戈泼,比如文件使用后自動關(guān)閉同诫、線程中鎖的自動獲取和釋放等咙俩。

術(shù)語

要使用 with 語句向胡,首先要明白上下文管理器這一概念矾湃。有了上下文管理器亡脑,with 語句才能工作。
下面是一組與上下文管理器和with 語句有關(guān)的概念邀跃。
上下文管理協(xié)議(Context Management Protocol):包含方法 __enter__() 和 __exit__()霉咨,支持該協(xié)議的對象要實現(xiàn)這兩個方法。
上下文管理器(Context Manager):支持上下文管理協(xié)議的對象拍屑,這種對象實現(xiàn)了__enter__() 和 __exit__() 方法途戒。上下文管理器定義執(zhí)行 with 語句時要建立的運行時上下文,負(fù)責(zé)執(zhí)行 with 語句塊上下文中的進入與退出操作僵驰。通常使用 with 語句調(diào)用上下文管理器喷斋,也可以通過直接調(diào)用其方法來使用。
運行時上下文(runtime context):由上下文管理器創(chuàng)建蒜茴,通過上下文管理器的 __enter__() 和__exit__() 方法實現(xiàn)星爪,__enter__() 方法在語句體執(zhí)行之前進入運行時上下文,__exit__() 在語句體執(zhí)行完后從運行時上下文退出粉私。with 語句支持運行時上下文這一概念顽腾。
上下文表達式(Context Expression):with 語句中跟在關(guān)鍵字 with 之后的表達式,該表達式要返回一個上下文管理器對象毡鉴。
語句體(with-body):with 語句包裹起來的代碼塊崔泵,在執(zhí)行語句體之前會調(diào)用上下文管理器的 __enter__() 方法,執(zhí)行完語句體之后會執(zhí)行 __exit__() 方法猪瞬。

基本語法和工作原理

with 語句的語法格式如下:
清單 1. with 語句的語法格式

with context_expression [as target(s)]:
    with-body

這里 context_expression 要返回一個上下文管理器對象憎瘸,該對象并不賦值給 as 子句中的 target(s) ,如果指定了 as 子句的話陈瘦,會將上下文管理器的 __enter__() 方法的返回值賦值給 target(s)幌甘。target(s) 可以是單個變量,或者由“()”括起來的元組(不能是僅僅由“,”分隔的變量列表痊项,必須加“()”)锅风。

Python 對一些內(nèi)建對象進行改進,加入了對上下文管理器的支持鞍泉,可以用于 with 語句中皱埠,比如可以自動關(guān)閉文件、線程鎖的自動獲取和釋放等咖驮。假設(shè)要對一個文件進行操作边器,使用 with 語句可以有如下代碼:

清單 2. 使用 with 語句操作文件對象

with open(r'somefileName') as somefile:
    for line in somefile:
        print line
        # ...more code

這里使用了 with 語句训枢,不管在處理文件過程中是否發(fā)生異常,都能保證 with 語句執(zhí)行完畢后已經(jīng)關(guān)閉了打開的文件句柄忘巧。如果使用傳統(tǒng)的 try/finally 范式恒界,則要使用類似如下代碼:

清單 3. try/finally 方式操作文件對象

somefile = open(r'somefileName')
try:
    for line in somefile:
        print line
        # ...more code
finally:
    somefile.close()

比較起來,使用 with 語句可以減少編碼量砚嘴。已經(jīng)加入對上下文管理協(xié)議支持的還有模塊 threading十酣、decimal 等。

PEP 0343 對 with 語句的實現(xiàn)進行了描述际长。with 語句的執(zhí)行過程類似如下代碼塊:

清單 4. with 語句執(zhí)行過程

context_manager = context_expression
exit = type(context_manager).__exit__  
value = type(context_manager).__enter__(context_manager)
exc = True   # True 表示正常執(zhí)行耸采,即便有異常也忽略;False 表示重新拋出異常也颤,需要對異常進行處理
try:
    try:
        target = value  # 如果使用了 as 子句
        with-body     # 執(zhí)行 with-body
    except:
        # 執(zhí)行過程中有異常發(fā)生
        exc = False
        # 如果 __exit__ 返回 True洋幻,則異常被忽略郁轻;如果返回 False翅娶,則重新拋出異常
        # 由外層代碼對異常進行處理
        if not exit(context_manager, *sys.exc_info()):
            raise
finally:
    # 正常退出,或者通過 statement-body 中的 break/continue/return 語句退出
    # 或者忽略異常退出
    if exc:
        exit(context_manager, None, None, None) 
    # 缺省返回 None好唯,None 在布爾上下文中看做是 False

1.執(zhí)行 context_expression竭沫,生成上下文管理器
2.context_manager調(diào)用上下文管理器的 __enter__() 方法;如果使用了 as 子句骑篙,則將 __enter__() 方法的返回值賦值給 as 子句中的 target(s)
3.執(zhí)行語句體 with-body
4.不管是否執(zhí)行過程中是否發(fā)生了異常蜕提,執(zhí)行上下文管理器的 __exit__() 方法,__exit__() 方法負(fù)責(zé)執(zhí)行“清理”工作靶端,如釋放資源等谎势。如果執(zhí)行過程中沒有出現(xiàn)異常,或者語句體中執(zhí)行了語句 break/continue/return杨名,則以 None 作為參數(shù)調(diào)用 __exit__(None, None, None) 脏榆;如果執(zhí)行過程中出現(xiàn)異常,則使用 sys.exc_info 得到的異常信息為參數(shù)調(diào)用 __exit__(exc_type, exc_value, exc_traceback)
5.出現(xiàn)異常時台谍,如果 __exit__(type, value, traceback) 返回 False须喂,則會重新拋出異常,讓with 之外的語句邏輯來處理異常趁蕊,這也是通用做法坞生;如果返回 True,則忽略異常掷伙,不再對異常進行處理

自定義上下文管理器

開發(fā)人員可以自定義支持上下文管理協(xié)議的類是己。自定義的上下文管理器要實現(xiàn)上下文管理協(xié)議所需要的 __enter__() 和 __exit__() 兩個方法:

  • context_manager.__enter__() :進入上下文管理器的運行時上下文,在語句體執(zhí)行前調(diào)用任柜。with 語句將該方法的返回值賦值給 as 子句中的 target卒废,如果指定了 as 子句的話
  • context_manager.__exit__(exc_type, exc_value, exc_traceback) :退出與上下文管理器相關(guān)的運行時上下文寒波,返回一個布爾值表示是否對發(fā)生的異常進行處理。參數(shù)表示引起退出操作的異常升熊,如果退出時沒有發(fā)生異常俄烁,則3個參數(shù)都為None。如果發(fā)生異常级野,返回True 表示不處理異常页屠,否則會在退出該方法后重新拋出異常以由 with 語句之外的代碼邏輯進行處理。如果該方法內(nèi)部產(chǎn)生異常蓖柔,則會取代由 statement-body 中語句產(chǎn)生的異常辰企。要處理異常時,不要顯示重新拋出異常况鸣,即不能重新拋出通過參數(shù)傳遞進來的異常潜索,只需要將返回值設(shè)置為 False 就可以了。之后懂酱,上下文管理代碼會檢測是否 __exit__() 失敗來處理異常

假設(shè)有一個資源 DummyResource整陌,這種資源需要在訪問前先分配,使用完后再釋放掉震放;分配操作可以放到 __enter__() 方法中,釋放操作可以放到 __exit__() 方法中霍掺。簡單起見蜕企,這里只通過打印語句來表明當(dāng)前的操作幸乒,并沒有實際的資源分配與釋放丐重。

清單 5. 自定義支持 with 語句的對象

class DummyResource:
    def __init__(self, tag):
        self.tag = tag
        print 'Resource [%s]' % tag
    def __enter__(self):
        print '[Enter %s]: Allocate resource.' % self.tag
        return self   # 可以返回不同的對象
    def __exit__(self, exc_type, exc_value, exc_tb):
        print '[Exit %s]: Free resource.' % self.tag
        if exc_tb is None:
            print '[Exit %s]: Exited without exception.' % self.tag
        else:
            print '[Exit %s]: Exited with exception raised.' % self.tag
            return False   # 可以省略,缺省的None也是被看做是False

DummyResource 中的 __enter__() 返回的是自身的引用,這個引用可以賦值給 as 子句中的 target 變量氧卧;返回值的類型可以根據(jù)實際需要設(shè)置為不同的類型鼠锈,不必是上下文管理器對象本身。

__exit__() 方法中對變量 exc_tb 進行檢測同欠,如果不為 None茎刚,表示發(fā)生了異常蚊荣,返回 False 表示需要由外部代碼邏輯對異常進行處理敲霍;注意到如果沒有發(fā)生異常,缺省的返回值為 None,在布爾環(huán)境中也是被看做 False,但是由于沒有異常發(fā)生,__exit__() 的三個參數(shù)都為 None,上下文管理代碼可以檢測這種情況嘹履,做正常處理腻扇。

下面在 with 語句中訪問 DummyResource :
清單 6. 使用自定義的支持 with 語句的對象

with DummyResource('Normal'):
    print '[with-body] Run without exceptions.'
 
with DummyResource('With-Exception'):
    print '[with-body] Run with exception.'
    raise Exception
    print '[with-body] Run with exception. Failed to finish statement-body!'

第1個 with 語句的執(zhí)行結(jié)果如下:
清單 7. with 語句1執(zhí)行結(jié)果

Resource [Normal]
[Enter Normal]: Allocate resource.
[with-body] Run without exceptions.
[Exit Normal]: Free resource.
[Exit Normal]: Exited without exception.

可以看到幼苛,正常執(zhí)行時會先執(zhí)行完語句體 with-body配并,然后執(zhí)行 __exit__() 方法釋放資源岩喷。
第2個 with 語句的執(zhí)行結(jié)果如下:
清單 8. with 語句2執(zhí)行結(jié)果

Resource [With-Exception]
[Enter With-Exception]: Allocate resource.
[with-body] Run with exception.
[Exit With-Exception]: Free resource.
[Exit With-Exception]: Exited with exception raised.
 
Traceback (most recent call last):
  File "G:/demo", line 20, in <module>
   raise Exception
Exception

可以看到,with-body 中發(fā)生異常時with-body 并沒有執(zhí)行完鲸阔,但資源會保證被釋放掉偷霉,同時產(chǎn)生的異常由 with 語句之外的代碼邏輯來捕獲處理。

可以自定義上下文管理器來對軟件系統(tǒng)中的資源進行管理渔扎,比如數(shù)據(jù)庫連接硫狞、共享資源的訪問控制等。Python 在線文檔 Writing Context Managers 提供了一個針對數(shù)據(jù)庫連接進行管理的上下文管理器的簡單范例晃痴。

contextlib 模塊

contextlib 模塊提供了3個對象:裝飾器 contextmanager残吩、函數(shù) nested 和上下文管理器 closing。使用這些對象倘核,可以對已有的生成器函數(shù)或者對象進行包裝泣侮,加入對上下文管理協(xié)議的支持,避免了專門編寫上下文管理器來支持 with 語句紧唱。

裝飾器 contextmanager

contextmanager 用于對生成器函數(shù)進行裝飾活尊,生成器函數(shù)被裝飾以后,返回的是一個上下文管理器琼蚯,其 __enter__() 和 __exit__() 方法由 contextmanager 負(fù)責(zé)提供酬凳,而不再是之前的迭代子。被裝飾的生成器函數(shù)只能產(chǎn)生一個值遭庶,否則會導(dǎo)致異常 RuntimeError;產(chǎn)生的值會賦值給 as 子句中的 target稠屠,如果使用了 as 子句的話峦睡。下面看一個簡單的例子。

清單 9. 裝飾器 contextmanager 使用示例

from contextlib import contextmanager
 
@contextmanager
def demo():
    print '[Allocate resources]'
    print 'Code before yield-statement executes in __enter__'
    yield '*** contextmanager demo ***'
    print 'Code after yield-statement executes in __exit__'
    print '[Free resources]'
 
with demo() as value:
    print 'Assigned Value: %s' % value

結(jié)果輸出如下:
清單 10. contextmanager 使用示例執(zhí)行結(jié)果

[Allocate resources]
Code before yield-statement executes in __enter__
Assigned Value: *** contextmanager demo ***
Code after yield-statement executes in __exit__
[Free resources]

可以看到权埠,生成器函數(shù)中 yield 之前的語句在 __enter__() 方法中執(zhí)行榨了,yield 之后的語句在 __exit__() 中執(zhí)行,而 yield 產(chǎn)生的值賦給了 as 子句中的 value 變量攘蔽。

需要注意的是龙屉,contextmanager 只是省略了 __enter__() / __exit__() 的編寫,但并不負(fù)責(zé)實現(xiàn)資源的“獲取”和“清理”工作;“獲取”操作需要定義在 yield 語句之前转捕,“清理”操作需要定義 yield 語句之后作岖,這樣 with 語句在執(zhí)行 __enter__() / __exit__() 方法時會執(zhí)行這些語句以獲取/釋放資源,即生成器函數(shù)中需要實現(xiàn)必要的邏輯控制五芝,包括資源訪問出現(xiàn)錯誤時拋出適當(dāng)?shù)漠惓痘儡!?/p>

函數(shù) nested

nested 可以將多個上下文管理器組織在一起,避免使用嵌套 with 語句枢步。
清單 11. nested 語法

with nested(A(), B(), C()) as (X, Y, Z):
     # with-body code here

類似于:
清單 12. nested 執(zhí)行過程

with A() as X:
    with B() as Y:
        with C() as Z:
             # with-body code here

需要注意的是沉删,發(fā)生異常后,如果某個上下文管理器的 __exit__() 方法對異常處理返回 False醉途,則更外層的上下文管理器不會監(jiān)測到異常矾瑰。

上下文管理器 closing

closing 的實現(xiàn)如下:
清單 13. 上下文管理 closing 實現(xiàn)

class closing(object):
    # help doc here
    def __init__(self, thing):
        self.thing = thing
    def __enter__(self):
        return self.thing
    def __exit__(self, *exc_info):
        self.thing.close()

上下文管理器會將包裝的對象賦值給 as 子句的 target 變量,同時保證打開的對象在 with-body 執(zhí)行完后會關(guān)閉掉隘擎。closing 上下文管理器包裝起來的對象必須提供 close() 方法的定義脯倚,否則執(zhí)行時會報 AttributeError 錯誤。
清單 14. 自定義支持 closing 的對象

class ClosingDemo(object):
    def __init__(self):
        self.acquire()
    def acquire(self):
        print 'Acquire resources.'
    def free(self):
        print 'Clean up any resources acquired.'
    def close(self):
        self.free()
 
with closing(ClosingDemo()):
    print 'Using resources'

結(jié)果輸出如下:
清單 15. 自定義 closing 對象的輸出結(jié)果

Acquire resources.
Using resources
Clean up any resources acquired.

closing 適用于提供了 close() 實現(xiàn)的對象嵌屎,比如網(wǎng)絡(luò)連接推正、數(shù)據(jù)庫連接等,也可以在自定義類時通過接口 close() 來執(zhí)行所需要的資源“清理”工作宝惰。

小結(jié)

本文對 with 語句的語法和工作機理進行了介紹植榕,并通過示例介紹了如何實現(xiàn)自定義的上下文管理器,最后介紹了如何使用 contextlib 模塊來簡化上下文管理器的編寫尼夺。

相關(guān)鏈接:
https://www.ibm.com/developerworks/cn/opensource/os-cn-pythonwith/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末尊残,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子淤堵,更是在濱河造成了極大的恐慌寝衫,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拐邪,死亡現(xiàn)場離奇詭異慰毅,居然都是意外死亡,警方通過查閱死者的電腦和手機扎阶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進店門汹胃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人东臀,你說我怎么就攤上這事着饥。” “怎么了惰赋?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵宰掉,是天一觀的道長。 經(jīng)常有香客問我,道長轨奄,這世上最難降的妖魔是什么孟害? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮戚绕,結(jié)果婚禮上纹坐,老公的妹妹穿的比我還像新娘。我一直安慰自己舞丛,他們只是感情好耘子,可當(dāng)我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著球切,像睡著了一般谷誓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吨凑,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天捍歪,我揣著相機與錄音,去河邊找鬼鸵钝。 笑死糙臼,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的恩商。 我是一名探鬼主播变逃,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼怠堪!你這毒婦竟也來了揽乱?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤粟矿,失蹤者是張志新(化名)和其女友劉穎凰棉,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體陌粹,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡撒犀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了申屹。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绘证。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖哗讥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情胞枕,我是刑警寧澤杆煞,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響决乎,放射性物質(zhì)發(fā)生泄漏队询。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一构诚、第九天 我趴在偏房一處隱蔽的房頂上張望蚌斩。 院中可真熱鬧,春花似錦范嘱、人聲如沸送膳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽叠聋。三九已至,卻和暖如春受裹,著一層夾襖步出監(jiān)牢的瞬間碌补,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工棉饶, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留厦章,地道東北人。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓照藻,卻偏偏與公主長得像袜啃,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子岩梳,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,440評論 2 359

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

  • 本文轉(zhuǎn)自淺談Python的with語句 引言 with 語句是從 Python 2.5 開始引入的一種與異常處理相...
    Syfun閱讀 4,008評論 0 50
  • 轉(zhuǎn)載自:http://mp.weixin.qq.com/s/LO1yyFeUA6pR_YPyfDoSig 姓名:梅...
    虐先森閱讀 1,424評論 0 1
  • contextlib — Context Manager Utilities contextlib - 上下文管理...
    英武閱讀 2,818評論 0 52
  • 術(shù)語 要使用 with 語句囊骤,首先要明白上下文管理器這一概念。有了上下文管理器冀值,with 語句才能工作也物。下面是一組...
    lmem閱讀 298評論 0 0
  • 我羨慕狂風(fēng) 無論它來去多么匆匆 卻總有云朵 就算同行的還有 雨水,樹葉和塵埃 我羨慕夏蟬 無論它生...
    顧平川閱讀 161評論 0 0