Python——with語句、context manager類型和contextlib庫


目錄

  一、with語句

  二、上下文管理器

  三、contextlib模塊

 

基本概念

上下文管理協議(Context Management Protocol)

  包含方法 __enter__() 和 __exit__(),支持該協議的對象要實現這兩個方法。

上下文管理器(Context Manager)

  支持上下文管理協議的對象,這種對象實現了__enter__() 和 __exit__() 方法。上下文管理器定義執行 with 語句時要建立的運行時上下文,負責執行 with 語句塊上下文中的進入與退出操作。通常使用 with 語句調用上下文管理器,也可以通過直接調用其方法來使用。

運行時上下文(runtime context)

  由上下文管理器創建,通過上下文管理器的 __enter__() 和__exit__() 方法實現,__enter__() 方法在語句體執行之前進入運行時上下文,__exit__() 在語句體執行完后從運行時上下文退出。with 語句支持運行時上下文這一概念。

上下文表達式(Context Expression)

  with 語句中跟在關鍵字 with 之后的表達式,該表達式要返回一個上下文管理器對象。

語句體(with-body)

  with 語句包裹起來的代碼塊,在執行語句體前會調用上下文管理器的 __enter__() 方法,執行完語句體之后會執行 __exit__() 方法。

 

一、with語句

  關於 Python 中 with 語句的詳細說明:PEP 343

  with 語句用上下文管理器定義的方法包裹一段代碼的執行,等價於簡單版的try...except...finally語句。with語句的主要針對的情境:不論一個代碼塊的執行過程是否出現異常,都要在結束的時候執行一些操作(比如清理)。

  with語句語法:

with_stmt ::=  "with" with_item ("," with_item)* ":" suite
with_item ::=  expression ["as" target]

  或:

with expression [as variable]:
    with-block

  expression應該返回一個支持“上下文管理協議”的對象,如果 as 分句存在的話,這個對象的返回值會被賦給 as 后面的變量名。

 

第一種語法表示中with語句的執行流程:

  1. 計算上下文表達式 (with_item 中給出的表達式) ,獲得一個上下文管理器,該上下文管理器包括__enter__() 和 __exit__()方法;
  2. 加載該上下文管理器的 __exit__() 方法留待以后使用;
  3. 調用上下文管理器的 __enter__() 方法;
  4. 如果 with 語句中的 with_item 指定了目標別名(as 分句后面的變量名),__enter__() 的返回值被賦給這個目標別名;

  *注

  with 語句確保只要 __enter__() 正常返回,那么 __exit__()一定會被調用。因此如果在將值賦給目標別名時發生錯誤,該錯誤將被當做發生在suite中。可以看下面的第6步。

  5. with語句中嵌套的 suite 部分被執行(第二種表示中的 with-block 部分);

  6. 上下文管理器的 __exit__() 方法被調用,如果是異常造成 suite(with-block) 部分退出,異常的類型、值和回溯都被當做參數傳給 __exit__(type, value, traceback) 方法,這三個值和sys.exc_info的返回值相同。如果suite(with-block) 部分沒有拋出異常,__exit__()的三個參數都是 None

  如果 suite 部分是由於異常導致的退出,且__exit__()方法的返回值是false,異常將被重舉;如果返回值是真,異常將被終止,with 語句后的代碼繼續執行。

  如果 suite 部分是由於不是異常的其他原因導致的退出,__exit__()方法的返回值被忽視,執行在退出發生的地方繼續

  單個上下文管理器示例:

with open(r'C:\misc\data') as myfile:
    for line in myfile:
        print(line)
        #...more code...

  Python中的文件對象包含會在with代碼段后自動關閉文件對象的上下文管理器,所以即便 for 循環中拋出處理文件對象時的異常,也能保證 myfile 引用的對象被正常關閉。

  如果這段代碼使用 try...finally...語句改寫:

myfile = open(r'C:\misc\data')
try:
    for line in myfile:
        print(line)
        #...more code...
finally:
    myfile.close()

  可見使用 with 語句能夠簡化代碼。

 

  多個上下文管理器被處理的方式就像多個 with 語句嵌套執行一樣:

with A() as a, B() as b:
    suite

  等價於:

with A() as a:
    with B() as b:
        suite

  多個上下文管理器示例:

with open('data') as fin, open(''res', 'w') as fout:
    for line in fin:
        if 'some key' in line:
            fout.write(line)

  

二、上下文管理器類型
  context manager 是Python中 with 語句執行時用來定義運行時上下文的對象,上下文管理器控制着 進 / 出 運行時上下文的功能,上下文管理器通常由 with 語句觸發,也可以直接通過調用他們的方法來使用他們。上下文管理器的通常用於保存和恢復各式各樣的全局狀態、加解鎖資源和關閉打開的文件等等。  

  Python的 with 語句支持由上下文管理器定義的運行時上下文,由兩個方法來實現,這兩個方法允許用戶在自定義的類中定義運行時上下文,執行流程在 with 語句開始前進入上下文,當 with 語句結束后退出。

上下文管理器必須提供一對方法:

  

 contextmanager.__enter__() 

  進入運行時上下文,要么返回該對象,要么返回一個與運行時上下文相關的對象,如果有 as 分句的話,with 語句將會把該方法的返回值和 as 分句指定的目標(別名)進行綁定。

  比如文件對象在__enter__()里返回自己,這樣 open() 函數可以被當做環境表達式在一個 with 語句中使用。

  另一種情形中, decimal.localcontext()返回一個相關的對象。上下文管理器將活躍的小數上下文設置為初始小數上下文的拷貝,然后返回拷貝后的引用。這樣可以在 with 語句中修改當前的小數上下文而不影響with 語句外的代碼。
  
  contextmanager.__exit__(exc_type, exc_val, exc_tb) 
  
  退出運行時上下文,參數是 with 代碼塊中拋出的異常的詳細信息,返回一個Bool型的標識符,該標識符說明上下文管理器能否處理異常:如果上下文管理器可以處理這個異常,__exit__() 應當返回一個true值來指示不需要傳播這個異常;如果返回false,就會導致__exit__() 返回后重新拋出這個異常, 如果上下文不是因為異常而退出,則三個參數都是  None
  該方法如果返回True ,說明上下文管理器可以處理異常,使得 with 語句終止異常傳播,然后直接執行 with 語句后的語句;否則異常會在該方法調用結束后正常傳播,該方法執行時發生的異常會替代所有其他在執行 with 代碼塊時出現的異常。
  在__exit__()中不需要顯式重舉傳入的異常,只需要返回 false 表明 __exit__() 正常執行且傳入的異常需要重舉即可,重舉傳入__exit__()方法的異常是調用__exit__()的函數的工作。這種機制便於上下文管理器檢測  __exit__() 方法是否真的執行失敗。
  Python定義了若干上下文管理器,Python的  generator 類型和裝飾器  contextlib.contextmanager 提供了實現上下文管理器協議的手段,如果一個生成器函數被裝飾器  contextlib.contextmanager 所裝飾,它將會返回一個實現了必要的  __enter__() 和 __exit__() 方法的上下文管理器,而不是普通的迭代器!

  Note that there is no specific slot for any of these methods in the type structure for Python objects in the Python/C API. Extension types wanting to define these methods must provide them as a normal Python accessible method. Compared to the overhead of setting up the runtime context, the overhead of a single class dictionary lookup is negligible.

   自定義上下文管理器示例:

class TraceBlock(object):

    def message(self, arg):
        print('running' + arg)

    def __enter__(self):
        print('starting with block')
        return self

    def __exit__(self, exc_type, exc_value, exc_tb):
        if exc_type is None:
            print('exited normally\n')
        else:
            print('raise an exception! ' + str(exc_type))
            return False #Propagate

if __name__ == '__main__':
    with TraceBlock() as action:
        action.message('test1')
        print('reached')

     with TraceBlock() as action:
        action.message('test2')
        raise TypeError
        print ('not reached')

  

三、contextlib模塊

  采用“二、上下文管理器類型”中說明的傳統方法即編寫一個包含__enter__()和__exit__()方法的類來創建上下文管理器並不難。不過有些時候,對於很少的上下文來說,完全編寫所有代碼會是額外的負擔。在這些情況下,可以使用裝飾器 contextlib.contextmanager() 將一個生成器函數轉換為上下文管理器。

  contextlib模塊提供對 with 語句的支持:

 contextlib.contextmanager(func) 

  裝飾器,用來裝飾一個生成器函數,使其成為一個上下文管理器。通過該裝飾器可以方便地定義上下文管理器,不必創建一個類或單獨指定__enter__() 和 __exit__() 方法。

  示例

  (該例子不應該用於實際生成HTML!):

from contextlib import contextmanager

@contextmanager
def tag(name):
    print "<%s>" % name
    yield
    print "</%s>" % name

>>> with tag("h1"):
...    print "foo"
...
<h1>
foo
</h1>

  上下文管理器的 __enter__() 和 __exit__() 方法由 @contextmanager 負責提供,不再是之前的迭代子。被裝飾的生成器函數只能產生一個值,否則會導致異常 RuntimeError;如果使用了 as 子句的話,產生的值會賦值給 as 子句中的 target。

  生成器函數中 yield 之前的語句在 __enter__() 方法中執行;yield 之后的語句在 __exit__() 中執行;yield 產生的值賦給 as 子句中的 variable 變量。

  生成器 yield 的地方,切換到執行 with 語句中的代碼塊(with-block),生成器在代碼塊退出后繼續執行。

  如果 with 代碼塊中有未被處理的異常,它會被重舉到生成器函數中的 yield 處,因此可以在生成器函數中使用 try...except...finally 語句來處理被重舉的異常

  如果捕獲異常的目標是記錄日志或執行一些操作(而不是抑制它),生成器必須重舉異常。否則生成器上下文管理器將會告知 with 語句說異常已經被處理了,但是一旦執行到 with 語句后的代碼,異常會立即繼續。

  需要注意的是,@contextmanager 只是省略了 __enter__() / __exit__() 的編寫,實際意義是減少了代碼量。

  但裝飾器@contextmanager並不負責資源的“獲取”和“清理”工作(需要開發者自己實現)。“獲取”操作需要定義在 yield 語句之前,“清理”操作需要定義 yield 語句之后,這樣 with 語句在執行 __enter__() / __exit__() 方法時會執行這些語句以獲取/釋放資源,即生成器函數中需要實現必要的邏輯控制,包括資源訪問出現錯誤時拋出適當的異常。

   示例:

from contextlib import contextmanager
  
@contextmanager
def make_context() :
    print 'enter'
    try :
        yield {}
    except RuntimeError, err :
        print 'error' , err
    finally :
        print 'exit'
  
with make_context() as value :
    print value

  with-block中拋出的異常將被重舉到生成器的 yield 處,在生成器中可以捕獲該異常。

  

 

 

 contextlib.closing(thing) 

  返回一個上下文管理器,在完成代碼塊的執行時關閉參數 thing。等價於:

from contextlib import contextmanager

@contextmanager
def closing(thing):
    try:
        yield thing
    finally:
        thing.close()

  可以這么使用:

from contextlib import closing
import urllib

with closing(urllib.urlopen('http://www.python.org')) as page:
    for line in page:
        print line

  該例中,不用顯式地關閉 page。即使發生錯誤,也會在 with 代碼塊退出時執行 page.close() 


免責聲明!

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



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