詳解Python中with語(yǔ)句的用法
引言
with 語(yǔ)句是從 Python 2.5 開(kāi)始引入的一種與異常處理相關(guān)的功能(2.5 版本中要通過(guò) from __future__ import with_statement 導(dǎo)入后才可以使用),從 2.6 版本開(kāi)始缺省可用(參考 What's new in Python 2.6? 中 with 語(yǔ)句相關(guān)部分介紹)。with 語(yǔ)句適用于對(duì)資源進(jìn)行訪問(wèn)的場(chǎng)合,確保不管使用過(guò)程中是否發(fā)生異常都會(huì)執(zhí)行必要的“清理”操作,釋放資源,比如文件使用后自動(dòng)關(guān)閉、線程中鎖的自動(dòng)獲取和釋放等。
術(shù)語(yǔ)
要使用 with 語(yǔ)句,首先要明白上下文管理器這一概念。有了上下文管理器,with 語(yǔ)句才能工作。
下面是一組與上下文管理器和with 語(yǔ)句有關(guān)的概念。
上下文管理協(xié)議(Context Management Protocol):包含方法 __enter__() 和 __exit__(),支持
該協(xié)議的對(duì)象要實(shí)現(xiàn)這兩個(gè)方法。
上下文管理器(Context Manager):支持上下文管理協(xié)議的對(duì)象,這種對(duì)象實(shí)現(xiàn)了
__enter__() 和 __exit__() 方法。上下文管理器定義執(zhí)行 with 語(yǔ)句時(shí)要建立的運(yùn)行時(shí)上下文,
負(fù)責(zé)執(zhí)行 with 語(yǔ)句塊上下文中的進(jìn)入與退出操作。通常使用 with 語(yǔ)句調(diào)用上下文管理器,
也可以通過(guò)直接調(diào)用其方法來(lái)使用。
運(yùn)行時(shí)上下文(runtime context):由上下文管理器創(chuàng)建,通過(guò)上下文管理器的 __enter__() 和
__exit__() 方法實(shí)現(xiàn),__enter__() 方法在語(yǔ)句體執(zhí)行之前進(jìn)入運(yùn)行時(shí)上下文,__exit__() 在
語(yǔ)句體執(zhí)行完后從運(yùn)行時(shí)上下文退出。with 語(yǔ)句支持運(yùn)行時(shí)上下文這一概念。
上下文表達(dá)式(Context Expression):with 語(yǔ)句中跟在關(guān)鍵字 with 之后的表達(dá)式,該表達(dá)式
要返回一個(gè)上下文管理器對(duì)象。
語(yǔ)句體(with-body):with 語(yǔ)句包裹起來(lái)的代碼塊,在執(zhí)行語(yǔ)句體之前會(huì)調(diào)用上下文管
理器的 __enter__() 方法,執(zhí)行完語(yǔ)句體之后會(huì)執(zhí)行 __exit__() 方法。
基本語(yǔ)法和工作原理
with 語(yǔ)句的語(yǔ)法格式如下:
清單 1. with 語(yǔ)句的語(yǔ)法格式
with context_expression [as target(s)]: with-body
這里 context_expression 要返回一個(gè)上下文管理器對(duì)象,該對(duì)象并不賦值給 as 子句中的 target(s) ,如果指定了 as 子句的話,會(huì)將上下文管理器的 __enter__() 方法的返回值賦值給 target(s)。target(s) 可以是單個(gè)變量,或者由“()”括起來(lái)的元組(不能是僅僅由“,”分隔的變量列表,必須加“()”)。
Python 對(duì)一些內(nèi)建對(duì)象進(jìn)行改進(jìn),加入了對(duì)上下文管理器的支持,可以用于 with 語(yǔ)句中,比如可以自動(dòng)關(guān)閉文件、線程鎖的自動(dòng)獲取和釋放等。假設(shè)要對(duì)一個(gè)文件進(jìn)行操作,使用 with 語(yǔ)句可以有如下代碼:
清單 2. 使用 with 語(yǔ)句操作文件對(duì)象
with open(r'somefileName') as somefile: for line in somefile: print line # ...more code
這里使用了 with 語(yǔ)句,不管在處理文件過(guò)程中是否發(fā)生異常,都能保證 with 語(yǔ)句執(zhí)行完畢后已經(jīng)關(guān)閉了打開(kāi)的文件句柄。如果使用傳統(tǒng)的 try/finally 范式,則要使用類似如下代碼:
清單 3. try/finally 方式操作文件對(duì)象
somefile = open(r'somefileName') try: for line in somefile: print line # ...more code finally: somefile.close()
比較起來(lái),使用 with 語(yǔ)句可以減少編碼量。已經(jīng)加入對(duì)上下文管理協(xié)議支持的還有模塊 threading、decimal 等。
PEP 0343 對(duì) with 語(yǔ)句的實(shí)現(xiàn)進(jìn)行了描述。with 語(yǔ)句的執(zhí)行過(guò)程類似如下代碼塊:
清單 4. with 語(yǔ)句執(zhí)行過(guò)程
context_manager = context_expression exit = type(context_manager).__exit__ value = type(context_manager).__enter__(context_manager) exc = True # True 表示正常執(zhí)行,即便有異常也忽略;False 表示重新拋出異常,需要對(duì)異常進(jìn)行處理 try: try: target = value # 如果使用了 as 子句 with-body # 執(zhí)行 with-body except: # 執(zhí)行過(guò)程中有異常發(fā)生 exc = False # 如果 __exit__ 返回 True,則異常被忽略;如果返回 False,則重新拋出異常 # 由外層代碼對(duì)異常進(jìn)行處理 if not exit(context_manager, *sys.exc_info()): raise finally: # 正常退出,或者通過(guò) statement-body 中的 break/continue/return 語(yǔ)句退出 # 或者忽略異常退出 if exc: exit(context_manager, None, None, None) # 缺省返回 None,None 在布爾上下文中看做是 False
執(zhí)行 context_expression,生成上下文管理器 context_manager
調(diào)用上下文管理器的 __enter__() 方法;如果使用了 as 子句,則將 __enter__() 方法的返回值賦值給 as 子句中的 target(s)
執(zhí)行語(yǔ)句體 with-body
不管是否執(zhí)行過(guò)程中是否發(fā)生了異常,執(zhí)行上下文管理器的 __exit__() 方法,__exit__() 方法負(fù)責(zé)執(zhí)行“清理”工作,如釋放資源等。如果執(zhí)行過(guò)程中沒(méi)有出現(xiàn)異常,或者語(yǔ)句體中執(zhí)行了語(yǔ)句 break/continue/return,則以 None 作為參數(shù)調(diào)用 __exit__(None, None, None) ;如果執(zhí)行過(guò)程中出現(xiàn)異常,則使用 sys.exc_info 得到的異常信息為參數(shù)調(diào)用 __exit__(exc_type, exc_value, exc_traceback)
出現(xiàn)異常時(shí),如果 __exit__(type, value, traceback) 返回 False,則會(huì)重新拋出異常,讓with 之外的語(yǔ)句邏輯來(lái)處理異常,這也是通用做法;如果返回 True,則忽略異常,不再對(duì)異常進(jìn)行處理
自定義上下文管理器
開(kāi)發(fā)人員可以自定義支持上下文管理協(xié)議的類。自定義的上下文管理器要實(shí)現(xiàn)上下文管理協(xié)議所需要的 __enter__() 和 __exit__() 兩個(gè)方法:
context_manager.__enter__() :進(jìn)入上下文管理器的運(yùn)行時(shí)上下文,在語(yǔ)句體執(zhí)行前調(diào)用。with 語(yǔ)句將該方法的返回值賦值給 as 子句中的 target,如果指定了 as 子句的話
context_manager.__exit__(exc_type, exc_value, exc_traceback) :退出與上下文管理器相關(guān)的運(yùn)行時(shí)上下文,返回一個(gè)布爾值表示是否對(duì)發(fā)生的異常進(jìn)行處理。參數(shù)表示引起退出操作的異常,如果退出時(shí)沒(méi)有發(fā)生異常,則3個(gè)參數(shù)都為None。如果發(fā)生異常,返回
True 表示不處理異常,否則會(huì)在退出該方法后重新拋出異常以由 with 語(yǔ)句之外的代碼邏輯進(jìn)行處理。如果該方法內(nèi)部產(chǎn)生異常,則會(huì)取代由 statement-body 中語(yǔ)句產(chǎn)生的異常。要處理異常時(shí),不要顯示重新拋出異常,即不能重新拋出通過(guò)參數(shù)傳遞進(jìn)來(lái)的異常,只需要將返回值設(shè)置為 False 就可以了。之后,上下文管理代碼會(huì)檢測(cè)是否 __exit__() 失敗來(lái)處理異常
下面通過(guò)一個(gè)簡(jiǎn)單的示例來(lái)演示如何構(gòu)建自定義的上下文管理器。注意,上下文管理器必須同時(shí)提供 __enter__() 和 __exit__() 方法的定義,缺少任何一個(gè)都會(huì)導(dǎo)致 AttributeError;with 語(yǔ)句會(huì)先檢查是否提供了 __exit__() 方法,然后檢查是否定義了 __enter__() 方法。
假設(shè)有一個(gè)資源 DummyResource,這種資源需要在訪問(wèn)前先分配,使用完后再釋放掉;分配操作可以放到 __enter__() 方法中,釋放操作可以放到 __exit__() 方法中。簡(jiǎn)單起見(jiàn),這里只通過(guò)打印語(yǔ)句來(lái)表明當(dāng)前的操作,并沒(méi)有實(shí)際的資源分配與釋放。
清單 5. 自定義支持 with 語(yǔ)句的對(duì)象
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 # 可以返回不同的對(duì)象 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__() 返回的是自身的引用,這個(gè)引用可以賦值給 as 子句中的 target 變量;返回值的類型可以根據(jù)實(shí)際需要設(shè)置為不同的類型,不必是上下文管理器對(duì)象本身。
__exit__() 方法中對(duì)變量 exc_tb 進(jìn)行檢測(cè),如果不為 None,表示發(fā)生了異常,返回 False 表示需要由外部代碼邏輯對(duì)異常進(jìn)行處理;注意到如果沒(méi)有發(fā)生異常,缺省的返回值為 None,在布爾環(huán)境中也是被看做 False,但是由于沒(méi)有異常發(fā)生,__exit__() 的三個(gè)參數(shù)都為 None,上下文管理代碼可以檢測(cè)這種情況,做正常處理。
下面在 with 語(yǔ)句中訪問(wèn) DummyResource :
清單 6. 使用自定義的支持 with 語(yǔ)句的對(duì)象
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個(gè) with 語(yǔ)句的執(zhí)行結(jié)果如下:
清單 7. with 語(yǔ)句1執(zhí)行結(jié)果
Resource [Normal]
[Enter Normal]: Allocate resource.
[with-body] Run without exceptions.
[Exit Normal]: Free resource.
[Exit Normal]: Exited without exception.
可以看到,正常執(zhí)行時(shí)會(huì)先執(zhí)行完語(yǔ)句體 with-body,然后執(zhí)行 __exit__() 方法釋放資源。
第2個(gè) with 語(yǔ)句的執(zhí)行結(jié)果如下:
清單 8. with 語(yǔ)句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ā)生異常時(shí)with-body 并沒(méi)有執(zhí)行完,但資源會(huì)保證被釋放掉,同時(shí)產(chǎn)生的異常由 with 語(yǔ)句之外的代碼邏輯來(lái)捕獲處理。
可以自定義上下文管理器來(lái)對(duì)軟件系統(tǒng)中的資源進(jìn)行管理,比如數(shù)據(jù)庫(kù)連接、共享資源的訪問(wèn)控制等。Python 在線文檔 Writing Context Managers 提供了一個(gè)針對(duì)數(shù)據(jù)庫(kù)連接進(jìn)行管理的上下文管理器的簡(jiǎn)單范例。
contextlib 模塊
contextlib 模塊提供了3個(gè)對(duì)象:裝飾器 contextmanager、函數(shù) nested 和上下文管理器 closing。使用這些對(duì)象,可以對(duì)已有的生成器函數(shù)或者對(duì)象進(jìn)行包裝,加入對(duì)上下文管理協(xié)議的支持,避免了專門編寫上下文管理器來(lái)支持 with 語(yǔ)句。
裝飾器 contextmanager
contextmanager 用于對(duì)生成器函數(shù)進(jìn)行裝飾,生成器函數(shù)被裝飾以后,返回的是一個(gè)上下文管理器,其 __enter__() 和 __exit__() 方法由 contextmanager 負(fù)責(zé)提供,而不再是之前的迭代子。被裝飾的生成器函數(shù)只能產(chǎn)生一個(gè)值,否則會(huì)導(dǎo)致異常 RuntimeError;產(chǎn)生的值會(huì)賦值給 as 子句中的 target,如果使用了 as 子句的話。下面看一個(gè)簡(jiǎn)單的例子。
清單 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 之前的語(yǔ)句在 __enter__() 方法中執(zhí)行,yield 之后的語(yǔ)句在 __exit__() 中執(zhí)行,而 yield 產(chǎn)生的值賦給了 as 子句中的 value 變量。
需要注意的是,contextmanager 只是省略了 __enter__() / __exit__() 的編寫,但并不負(fù)責(zé)實(shí)現(xiàn)資源的“獲取”和“清理”工作;“獲取”操作需要定義在 yield 語(yǔ)句之前,“清理”操作需要定義 yield 語(yǔ)句之后,這樣 with 語(yǔ)句在執(zhí)行 __enter__() / __exit__() 方法時(shí)會(huì)執(zhí)行這些語(yǔ)句以獲取/釋放資源,即生成器函數(shù)中需要實(shí)現(xiàn)必要的邏輯控制,包括資源訪問(wèn)出現(xiàn)錯(cuò)誤時(shí)拋出適當(dāng)?shù)漠惓!?br /> 函數(shù) nested
nested 可以將多個(gè)上下文管理器組織在一起,避免使用嵌套 with 語(yǔ)句。
清單 11. nested 語(yǔ)法
with nested(A(), B(), C()) as (X, Y, Z): # with-body code here
類似于:
清單 12. nested 執(zhí)行過(guò)程
with A() as X: with B() as Y: with C() as Z: # with-body code here
需要注意的是,發(fā)生異常后,如果某個(gè)上下文管理器的 __exit__() 方法對(duì)異常處理返回 False,則更外層的上下文管理器不會(huì)監(jiān)測(cè)到異常。
上下文管理器 closing
closing 的實(shí)現(xiàn)如下:
清單 13. 上下文管理 closing 實(shí)現(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()
上下文管理器會(huì)將包裝的對(duì)象賦值給 as 子句的 target 變量,同時(shí)保證打開(kāi)的對(duì)象在 with-body 執(zhí)行完后會(huì)關(guān)閉掉。closing 上下文管理器包裝起來(lái)的對(duì)象必須提供 close() 方法的定義,否則執(zhí)行時(shí)會(huì)報(bào) AttributeError 錯(cuò)誤。
清單 14. 自定義支持 closing 的對(duì)象
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 對(duì)象的輸出結(jié)果
Acquire resources. Using resources Clean up any resources acquired.
closing 適用于提供了 close() 實(shí)現(xiàn)的對(duì)象,比如網(wǎng)絡(luò)連接、數(shù)據(jù)庫(kù)連接等,也可以在自定義類時(shí)通過(guò)接口 close() 來(lái)執(zhí)行所需要的資源“清理”工作。
相關(guān)文章
Python實(shí)現(xiàn)為PDF大文件批量去除水印
在閱讀過(guò)程中如果遇到一些帶有水印的資料是比較煩心的,而市面上去水印的功能有多要收費(fèi)且很不方便,那么,如何通過(guò)Python來(lái)對(duì)這類圖片水印進(jìn)行去除呢,本文就來(lái)和大家分享一下實(shí)現(xiàn)方法吧2023-05-05解決python將xml格式文件轉(zhuǎn)換成txt文件的問(wèn)題(xml.etree方法)
從數(shù)據(jù)分析的角度去看xml格式的數(shù)據(jù)集,具有簡(jiǎn)單性,結(jié)構(gòu)和內(nèi)容分離、可擴(kuò)展性的特征,今天通過(guò)本文給大家分享python將xml格式文件轉(zhuǎn)換成txt文件的問(wèn)題及解決方法(xml.etree方法),感興趣的朋友一起看看吧2021-09-09Python matplotlib超詳細(xì)教程實(shí)現(xiàn)圖形繪制
matplotlib 模塊不僅提供了繪制統(tǒng)計(jì)圖表的功能,還支持繪制圓形、正方形、矩形等各種圖形。這篇文章主要為大家詳細(xì)介紹了利用matplotlib.patches 繪制一些基本圖形,快來(lái)跟隨小編一起學(xué)習(xí)吧2021-12-12django配置連接數(shù)據(jù)庫(kù)及原生sql語(yǔ)句的使用方法
這篇文章主要給大家介紹了關(guān)于django配置連接數(shù)據(jù)庫(kù),以及原生sql語(yǔ)句的使用方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03pip?install?python-Levenshtein失敗的解決
這篇文章主要介紹了pip?install?python-Levenshtein失敗的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02詳解python3中用HTMLTestRunner.py報(bào)ImportError: No module named ''
這篇文章主要介紹了詳解python3中用HTMLTestRunner.py報(bào)ImportError: No module named 'StringIO'如何解決,感興趣的可以了解一下2019-08-08用Python實(shí)現(xiàn)通過(guò)哈希算法檢測(cè)圖片重復(fù)的教程
這篇文章主要介紹了用Python實(shí)現(xiàn)通過(guò)哈希算法檢測(cè)圖片重復(fù)的教程,這個(gè)方法被Iconfinder用作防盜版技術(shù),需要的朋友可以參考下2015-04-04