[Python Study Notes]with的使用
在 Python 2.5 中, with 關鍵字被加入。它將常用的 try ... except ... finally ... 模式很方便的被復用。看一個最經典的例子:
with open(‘file.txt‘) as f: content = f.read()
在這段代碼中,無論 with 中的代碼塊在執行的過程中發生任何情況,文件最終都會被關閉。如果代碼塊在執行的過程中發生了一個異常,那麽在這個異常被拋出前,程序會先將被打開的文件關閉。
再看另外一個例子。
在發起一個數據庫事務請求的時候,經常會用類似這樣的代碼:
db.begin() try: # do some actions except: db.rollback() raise finally: db.commit()
如果將發起事務請求的操作變成可以支持 with 關鍵字的,那麽用像這樣的代碼就可以了:
with transaction(db): # do some actions
下面,詳細的說明一下 with 的執行過程,並用兩種常用的方式實現上面的代碼。
with 的一般執行過程
一段基本的 with 表達式,其結構是這樣的:
with EXPR as VAR: BLOCK
其中: EXPR 可以是任意表達式; as VAR 是可選的。其一般的執行過程是這樣的:
- 計算 EXPR ,並獲取一個上下文管理器。
- 上下文管理器的 __exit()__ 方法被保存起來用於之後的調用。
- 調用上下文管理器的 __enter()__ 方法。
- 如果 with 表達式包含 as VAR ,那麽 EXPR 的返回值被賦值給 VAR 。
- 執行 BLOCK 中的表達式。
- 調用上下文管理器的 __exit()__ 方法。如果 BLOCK 的執行過程中發生了一個異常導致程序退出,那麽異常的 type 、 value 和 traceback (即 sys.exc_info()的返回值 )將作為參數傳遞給 __exit()__ 方法。否則,將傳遞三個 None 。
將這個過程用代碼表示,是這樣的:
mgr = (EXPR) exit = type(mgr).__exit__ # 這裏沒有執行 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)
這個過程有幾個細節:
如果上下文管理器中沒有 __enter()__ 或者 __exit()__ 中的任意一個方法,那麽解釋器會拋出一個 AttributeError 。
在 BLOCK 中發生異常後,如果 __exit()__ 方法返回一個可被看成是 True 的值,那麽這個異常就不會被拋出,後面的代碼會繼續執行。
接下來,用兩種方法來實現上面來實現上面的過程的吧。
實現上下文管理器類
第一種方法是實現一個類,其含有一個實例屬性 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 的執行過程後,這個實現方式是很容易理解的。下面介紹的實現方式,其原理理解起來要復雜很多。
使用生成器裝飾器
在Python的標準庫中,有一個裝飾器可以通過生成器獲取上下文管理器。使用生成器裝飾器的實現過程如下:
from contextlib import contextmanager @contextmanager def transaction(db): db.begin() try: yield db except: db.rollback() raise else: db.commit()
第一眼上看去,這種實現方式更為簡單,但是其機制更為復雜。看一下其執行過程吧:
- Python解釋器識別到 yield 關鍵字後, def 會創建一個生成器函數替代常規的函數(在類定義之外我喜歡用函數代替方法)。
- 裝飾器 contextmanager 被調用並返回一個幫助方法,這個幫助函數在被調用後會生成一個 GeneratorContextManager 實例。最終 with 表達式中的 EXPR 調用的是由 contentmanager 裝飾器返回的幫助函數。
- with 表達式調用 transaction(db) ,實際上是調用幫助函數。幫助函數調用生成器函數,生成器函數創建一個生成器。
- 幫助函數將這個生成器傳遞給 GeneratorContextManager ,並創建一個 GeneratorContextManager 的實例對象作為上下文管理器。
- with 表達式調用實例對象的上下文管理器的 __enter()__ 方法。
- __enter()__ 方法中會調用這個生成器的 next() 方法。這時候,生成器方法會執行到 yield db 處停止,並將 db 作為 next() 的返回值。如果有 as VAR ,那麽它將會被賦值給 VAR 。
- with 中的 BLOCK 被執行。
- BLOCK 執行結束後,調用上下文管理器的 __exit()__ 方法。 __exit()__ 方法會再次調用生成器的 next() 方法。如果發生 StopIteration 異常,則 pass 。
- 如果沒有發生異常生成器方法將會執行 db.commit() ,否則會執行 db.rollback() 。
再次看看上述過程的代碼大致實現:
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
總結
Python的 with 表達式包含了很多Python特性。花點時間吃透 with 是一件非常值得的事情。
一些其他的例子
鎖機制
@contextmanager def locked(lock): lock.acquired() try: yield finally: lock.release()
標準輸出重定向
@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"
[Python Study Notes]with的使用