- with語句
- 上下文管理器
- contextlib模塊
- 參考引用
with語句
with語句時在Python2.6中出現的新語句。在Python2.6以前,要正確的處理涉及到異常的資源管理時,需要使用try/finally代碼結構。如要實現文件在操作出現異常時也能正確關閉,則需要像如下實現:
f = open("test.txt") try: for line in f.readlines(): print(line) finally: f.close()
不管文件操作有沒有出現異常,try/finally中的finnally語句都會執行,從而保證文件的正確關閉。但是很顯然Python的設計者們並沒有滿足於此,他們以希望更簡潔更優美的形式來實現資源的清理,而且希望這種清理工作不需要暴露給使用者,所以便出現了with語句。
with語句的基本語法結構如下:
with expression [as variable]:
with-block
先看下如果用with語句代替上面的try/finally的例子,然后再討論它的更多細節,如下
with open("text.txt") as f: for line in f.readlines()
print(line)
是不是發現使用with語句相對try/finally來說簡潔了很多,而且也不需要每一個用戶都去寫f.close()來關閉文件了,這是因為with語句在背后做了大量的工作。with語句的expression是上下文管理器,這個我們下文會說。with語句中的[as variable]是可選的,如果指定了as variable說明符,則variable是上下文管理器expression調用__enter__()函數返回的對象。所以,f並不一定就是expression,而是expression.__enter__()的返回值,至於expression.__enter__()返回什么就由這個函數來決定了。with-block是執行語句,with-block執行完畢時,with語句會自動進行資源清理,對應上面例子就是with語句會自動關閉文件。
下面我們來具體說下with語句在背后默默無聞地到底做了哪些事情。剛才我們說了expression是一個上下文管理器,其實現了__enter__和__exit__兩個函數。當我們調用一個with語句時,執行過程如下:
1.首先生成一個上下文管理器expression,在上面例子中with語句首先以“test.txt”作為參數生成一個上下文管理器open("test.txt")。
2.然后執行expression.__enter__()。如果指定了[as variable]說明符,將__enter__()的返回值賦給variable。上例中open("test.txt").__enter__()返回的是一個文件對象給f。
3.執行with-block語句塊。上例中執行讀取文件。
4.執行expression.__exit__(),在__exit__()函數中可以進行資源清理工作。上面例子中就是執行文件的關閉操作。
with語句不僅可以管理文件,還可以管理鎖、連接等等,如下面的例子:
#管理鎖 import threading lock = threading.lock() with lock: #執行一些操作 pass
上下文管理器
在上文中我們提到with語句中的上下文管理器。with語句可以如此簡單但強大,主要依賴於上下文管理器。那么什么是上下文管理器?上下文管理器就是實現了上下文協議的類,而上下文協議就是一個類要實現__enter__()和__exit__()兩個方法。一個類只要實現了__enter__()和__exit__(),我們就稱之為上下文管理器下面我們具體說下這兩個方法。
__enter__():主要執行一些環境准備工作,同時返回一資源對象。如果上下文管理器open("test.txt")的__enter__()函數返回一個文件對象。
__exit__():完整形式為__exit__(type, value, traceback),這三個參數和調用sys.exec_info()函數返回值是一樣的,分別為異常類型、異常信息和堆棧。如果執行體語句沒有引發異常,則這三個參數均被設為None。否則,它們將包含上下文的異常信息。__exit_()方法返回True或False,分別指示被引發的異常有沒有被處理,如果返回False,引發的異常將會被傳遞出上下文。如果__exit__()函數內部引發了異常,則會覆蓋掉執行體的中引發的異常。處理異常時,不需要重新拋出異常,只需要返回False,with語句會檢測__exit__()返回False來處理異常。
如果我們要自定義一個上下文管理器,只需要定義一個類並且是實現__enter__()和__exit__()即可。下面通過一個簡單的例子是演示如果新建自定義的上下文管理器,我們以數據庫的連接為例。在使用數據庫時,有時要涉及到事務操作。數據庫的事務操作當調用commit()執行sql命令時,如果在這個過程中執行失敗,則需要執行rollback()回滾數據庫,通常實現方式可能如下:
def test_write(): con = MySQLdb.connection() cursor = con.cursor() sql = """ #具體的sql語句 """ try: cursor.execute(sql) cursor.execute(sql) cursor.execute(sql) con.commit() #提交事務 except Exception as ex: con.rollback() #事務執行失敗,回滾數據庫
如果想通過with語句來實現數據庫執行失敗的回滾操作,則我們需要自定義一個數據庫連接的上下文管理器,假設為DBConnection,則我們將上面例子用with語句來實現的話,應該是這樣子的,如下:
def test_write(): sql = """ #具體的sql語句 """ con = DBConnection() with con as cursor: cursor.execute(sql) cursor.execute(sql) cursor.execute(sql)
要實現上面with語句的功能,則我們的DBConnection數據庫上下文管理器則需要提供一下功能:__enter__()要返回一個連接的cursor; 當沒有異常發生是,__exit__()函數commit所有的數據庫操作。如果有異常發生則_exit__()會回滾數據庫,調用rollback()。所以我們可以實現DBConnection如下:
1 def DBConnection(object): 2 def __init__(self): 3 pass 4 5 def cursor(self): 6 #返回一個游標並且啟動一個事務 7 pass 8 9 def commit(self): 10 #提交當前事務 11 pass 12 13 def rollback(self): 14 #回滾當前事務 15 pass 16 17 def __enter__(self): 18 #返回一個cursor 19 cursor = self.cursor() 20 return cursor 21 22 def __exit__(self, type, value, tb): 23 if tb is None: 24 #沒有異常則提交事務 25 self.commit() 26 else: 27 #有異常則回滾數據庫 28 self.rollback()
contextlib模塊
- contextmanage對象
上文提到如果我們要實現一個自定義的上下文管理器,需要定義一個實現了__enter__和__exit__兩個方法的類, 這顯示不是很方便。Python的contextlib模塊給我們提供了更方便的方式來實現一個自定義的上下文管理器。contextlib模塊包含一個裝飾器contextmanager和一些輔助函數,裝飾器contextmanager只需要寫一個生成器函數就可以代替自定義的上下文管理器,典型用法如下:
需要使用yield先定義一個生成器函數.
@contextmanager def some_generator(<arguments>): <setup> try: yield <value> finally: <cleanup>
然后便可以用with語句調用contextmanage生成的上下文管理器了,with語句用法如下:
with some_generator(<arguments>) as <variable>:
<body>
生成器函數some_generator就和我們普通的函數一樣,它的原理如下:
- some_generator函數在在yield之前的代碼等同於上下文管理器中的__enter__函數。
- yield的返回值等同於__enter__函數的返回值,即如果with語句聲明了as <variable>,則yield的值會賦給variable
- 然后執行<cleanup>代碼塊,等同於上下文管理器的__exit__函數。此時發生的任何異常都會再次通過yield函數返回。
下面舉幾個簡單的例子,
例子1:鎖資源自動獲取和釋放的例子
1 @contextmanager 2 def locked(lock): 3 lock.acquire() 4 try: 5 yield 6 finally: 7 lock.release() 8 9 with locked(myLock): 10 #代碼執行到這里時,myLock已經自動上鎖 11 pass 12 #執行完后會,會自動釋放鎖
例子2:文件打開后自動管理的實現
1 @contextmanager 2 def myopen(filename, mode="r"): 3 f = open(filename,mode) 4 try: 5 yield f 6 finally: 7 f.close() 8 9 with myopen("test.txt") as f: 10 for line in f: 11 print(line)
例子3:數據庫事務的處理
@contextmanager def transaction(db): db.begin() try: yield except: db.rollback() raise else: db.commit() with transaction(mydb): mydb.cursor.execute(sql) mydb.cursor.execute(sql) mydb.cursor.execute(sql) mydb.cursor.execute(sql)
- nested函數
contextlib模塊還提供了一個函數給我們:nested(mgr1,mgr2...mgrn)函數,用來嵌套多個上下文管理器,等同於下面的形式:
with mgr1: with mgr2: ... with mgrn: pass
但是with語句本身已經支持了多個下文管理器的使用,所以nested的意義不是很大。我們可以寫一個例子來看下nested函數的使用,以及與直接使用with來嵌套多個上下文管理器的區別,如下所示:
1 from contextlib import contextmanager 2 from contextlib import nested 3 from contextlib import closing 4 5 @contextmanager 6 def my_context(name): 7 print("enter") 8 try: 9 yield name 10 finally: 11 print("exit") 12 13 #使用nested函數來調用多個管理器 14 print("---------使用nested函數調用多個管理器-----------") 15 with nested(my_context("管理器一"), my_context("管理器二"),my_context("管理器三")) as (m1,m2,m3): 16 print(m1) 17 print(m2) 18 print(m3) 19 20 #直接使用with來調用調用多個管理器 21 print("---------使用with調用多個管理器-----------") 22 with my_context("管理器一") as m1, my_context("管理器二") as m2, my_context("管理器三") as m3: 23 print(m1) 24 print(m2) 25 print(m3)
輸出結果為:
- closing對象
contextlib中還包含一個closing對象,這個對象就是一個上下文管理器,它的__exit__函數僅僅調用傳入參數的close函數,closing對象的源碼如下:
1 class closing(object): 18 def __init__(self, thing): 19 self.thing = thing 20 def __enter__(self): 21 return self.thing 22 def __exit__(self, *exc_info): 23 self.thing.close()
所以closeing上下文管理器僅使用於具有close()方法的資源對象。例如,如果我們通過urllib.urlopen打開一個網頁,urlopen返回的request有close方法,所以我們就可以使用closing上下文管理器,如下:
import urllib, sys from contextlib import closing with closing(urllib.urlopen('http://www.yahoo.com')) as f: for line in f: sys.stdout.write(line)
參考引用