【Python學習筆記】with語句與上下文管理器


 

  • 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就和我們普通的函數一樣,它的原理如下:

  1. some_generator函數在在yield之前的代碼等同於上下文管理器中的__enter__函數。
  2. yield的返回值等同於__enter__函數的返回值,即如果with語句聲明了as <variable>,則yield的值會賦給variable
  3. 然后執行<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()
    tryyield 
    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)

 

 

參考引用


 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM