Python 中的with關(guān)鍵字使用詳解
在 Python 2.5 中, with 關(guān)鍵字被加入。它將常用的 try ... except ... finally ... 模式很方便的被復(fù)用??匆粋€(gè)最經(jīng)典的例子:
with open('file.txt') as f: content = f.read()
在這段代碼中,無論 with 中的代碼塊在執(zhí)行的過程中發(fā)生任何情況,文件最終都會(huì)被關(guān)閉。如果代碼塊在執(zhí)行的過程中發(fā)生了一個(gè)異常,那么在這個(gè)異常被拋出前,程序會(huì)先將被打開的文件關(guān)閉。
再看另外一個(gè)例子。
在發(fā)起一個(gè)數(shù)據(jù)庫事務(wù)請求的時(shí)候,經(jīng)常會(huì)用類似這樣的代碼:
db.begin() try: # do some actions except: db.rollback() raise finally: db.commit()
如果將發(fā)起事務(wù)請求的操作變成可以支持 with 關(guān)鍵字的,那么用像這樣的代碼就可以了:
with transaction(db): # do some actions
下面,詳細(xì)的說明一下 with 的執(zhí)行過程,并用兩種常用的方式實(shí)現(xiàn)上面的代碼。
with 的一般執(zhí)行過程
一段基本的 with 表達(dá)式,其結(jié)構(gòu)是這樣的:
with EXPR as VAR: BLOCK
其中: EXPR 可以是任意表達(dá)式; as VAR 是可選的。其一般的執(zhí)行過程是這樣的:
- 計(jì)算 EXPR ,并獲取一個(gè)上下文管理器。
- 上下文管理器的 __exit()__ 方法被保存起來用于之后的調(diào)用。
- 調(diào)用上下文管理器的 __enter()__ 方法。
- 如果 with 表達(dá)式包含 as VAR ,那么 EXPR 的返回值被賦值給 VAR 。
- 執(zhí)行 BLOCK 中的表達(dá)式。
- 調(diào)用上下文管理器的 __exit()__ 方法。如果 BLOCK 的執(zhí)行過程中發(fā)生了一個(gè)異常導(dǎo)致程序退出,那么異常的 type 、 value 和 traceback (即 sys.exc_info()的返回值 )將作為參數(shù)傳遞給 __exit()__ 方法。否則,將傳遞三個(gè) None 。
將這個(gè)過程用代碼表示,是這樣的:
mgr = (EXPR) exit = type(mgr).__exit__ # 這里沒有執(zhí)行 value = type(mgr).__enter__(mgr) exc = True try: try: VAR = value # 如果有 as VAR BLOCK except: exc = False if not exit(mgr, *sys.exc_info()): raise finally: if exc: exit(mgr, None, None, None)
這個(gè)過程有幾個(gè)細(xì)節(jié):
如果上下文管理器中沒有 __enter()__ 或者 __exit()__ 中的任意一個(gè)方法,那么解釋器會(huì)拋出一個(gè) AttributeError 。
在 BLOCK 中發(fā)生異常后,如果 __exit()__ 方法返回一個(gè)可被看成是 True 的值,那么這個(gè)異常就不會(huì)被拋出,后面的代碼會(huì)繼續(xù)執(zhí)行。
接下來,用兩種方法來實(shí)現(xiàn)上面來實(shí)現(xiàn)上面的過程的吧。
實(shí)現(xiàn)上下文管理器類
第一種方法是實(shí)現(xiàn)一個(gè)類,其含有一個(gè)實(shí)例屬性 db 和上下文管理器所需要的方法 __enter()__ 和 __exit()__ 。
class transaction(object): def __init__(self, db): self.db = db def __enter__(self): self.db.begin() def __exit__(self, type, value, traceback): if type is None: db.commit() else: db.rollback()
了解 with 的執(zhí)行過程后,這個(gè)實(shí)現(xiàn)方式是很容易理解的。下面介紹的實(shí)現(xiàn)方式,其原理理解起來要復(fù)雜很多。
使用生成器裝飾器
在Python的標(biāo)準(zhǔn)庫中,有一個(gè)裝飾器可以通過生成器獲取上下文管理器。使用生成器裝飾器的實(shí)現(xiàn)過程如下:
from contextlib import contextmanager @contextmanager def transaction(db): db.begin() try: yield db except: db.rollback() raise else: db.commit()
第一眼上看去,這種實(shí)現(xiàn)方式更為簡單,但是其機(jī)制更為復(fù)雜??匆幌缕鋱?zhí)行過程吧:
- Python解釋器識別到 yield 關(guān)鍵字后, def 會(huì)創(chuàng)建一個(gè)生成器函數(shù)替代常規(guī)的函數(shù)(在類定義之外我喜歡用函數(shù)代替方法)。
- 裝飾器 contextmanager 被調(diào)用并返回一個(gè)幫助方法,這個(gè)幫助函數(shù)在被調(diào)用后會(huì)生成一個(gè) GeneratorContextManager 實(shí)例。最終 with 表達(dá)式中的 EXPR 調(diào)用的是由 contentmanager 裝飾器返回的幫助函數(shù)。
- with 表達(dá)式調(diào)用 transaction(db) ,實(shí)際上是調(diào)用幫助函數(shù)。幫助函數(shù)調(diào)用生成器函數(shù),生成器函數(shù)創(chuàng)建一個(gè)生成器。
- 幫助函數(shù)將這個(gè)生成器傳遞給 GeneratorContextManager ,并創(chuàng)建一個(gè) GeneratorContextManager 的實(shí)例對象作為上下文管理器。
- with 表達(dá)式調(diào)用實(shí)例對象的上下文管理器的 __enter()__ 方法。
- __enter()__ 方法中會(huì)調(diào)用這個(gè)生成器的 next() 方法。這時(shí)候,生成器方法會(huì)執(zhí)行到 yield db 處停止,并將 db 作為 next() 的返回值。如果有 as VAR ,那么它將會(huì)被賦值給 VAR 。
- with 中的 BLOCK 被執(zhí)行。
- BLOCK 執(zhí)行結(jié)束后,調(diào)用上下文管理器的 __exit()__ 方法。 __exit()__ 方法會(huì)再次調(diào)用生成器的 next() 方法。如果發(fā)生 StopIteration 異常,則 pass 。
- 如果沒有發(fā)生異常生成器方法將會(huì)執(zhí)行 db.commit() ,否則會(huì)執(zhí)行 db.rollback() 。
再次看看上述過程的代碼大致實(shí)現(xiàn):
def contextmanager(func): def helper(*args, **kwargs): return GeneratorContextManager(func(*args, **kwargs)) return helper class GeneratorContextManager(object): def __init__(self, gen): self.gen = gen def __enter__(self): try: return self.gen.next() except StopIteration: raise RuntimeError("generator didn't yield") def __exit__(self, type, value, traceback): if type is None: try: self.gen.next() except StopIteration: pass else: raise RuntimeError("generator didn't stop") else: try: self.gen.throw(type, value, traceback) raise RuntimeError("generator didn't stop after throw()") except StopIteration: return True except: if sys.exc_info()[1] is not value: raise
總結(jié)
Python的 with 表達(dá)式包含了很多Python特性?;c(diǎn)時(shí)間吃透 with 是一件非常值得的事情。
一些其他的例子
鎖機(jī)制
@contextmanager def locked(lock): lock.acquired() try: yield finally: lock.release()
標(biāo)準(zhǔn)輸出重定向
@contextmanager def stdout_redirect(new_stdout): old_stdout = sys.stdout sys.stdout = new_stdout try: yield finally: sys.stdout = old_stdout with open("file.txt", "w") as f: with stdout_redirect(f): print "hello world"
參考資料
相關(guān)文章
Python Pandas 如何shuffle(打亂)數(shù)據(jù)
這篇文章主要介紹了Python Pandas 如何shuffle(打亂)數(shù)據(jù),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07解決pyinstaller打包運(yùn)行程序時(shí)出現(xiàn)缺少plotly庫問題
這篇文章主要介紹了解決pyinstaller打包運(yùn)行程序時(shí)出現(xiàn)缺少plotly庫問題,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06Python 矩陣轉(zhuǎn)置的幾種方法小結(jié)
今天小編就為大家分享一篇Python 矩陣轉(zhuǎn)置的幾種方法小結(jié),具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-12-12