淺談Python的with語句

1. 引言

with 語句是從 Python 2.5 開始引入的一種與異常處理相關的功能(2.5 版本中要經過 from __future__ import with_statement 導入後纔可使用),從 2.6 版本開始缺省可用。with 語句適用於對資源進行訪問的場合,確保無論使用過程當中是否發生異常都會執行必要的「清理」操做,釋放資源,好比文件使用後自動關閉、線程中鎖的自動獲取和釋放等。html

2.術語

要使用 with 語句,首先要明白上下文管理器這一律念。有了上下文管理器,with 語句才能工做。下面是一組與上下文管理器和with 語句有關的概念。python

  • 上下文管理協議(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__() 方法。

3. 基本語法和工做原理

with 語句的語法格式以下:數據庫

清單 1. with 語句的語法格式
with context_expression [as target(s)]:
    with-body

這裏 context_expression 要返回一個上下文管理器對象,該對象並不賦值給 as 子句中的 target(s) ,若是指定了 as 子句的話,會將上下文管理器的 __enter__() 方法的返回值賦值給 target(s)。target(s) 能夠是單個變量,或者由「()」括起來的元組。express

Python 對一些內建對象進行改進,加入了對上下文管理器的支持,能夠用於 with 語句中,好比能夠自動關閉文件、線程鎖的自動獲取和釋放等。假設要對一個文件進行操做,使用 with 語句能夠有以下代碼:網絡

清單 2. 使用 with 語句操做文件對象
with open(r'somefileName') as somefile:
    for line in somefile:
        print line
        # ...more code

這裏使用了 with 語句,無論在處理文件過程當中是否發生異常,都能保證 with 語句執行完畢後已經關閉了打開的文件句柄。若是使用傳統的 try/finally 範式,則要使用相似以下代碼:函數

清單 3. try/finally 方式操做文件對象
somefile = open(r'somefileName')
try:
    for line in somefile:
        print line
        # ...more code
finally:
    somefile.close()

比較起來,使用 with 語句能夠減小編碼量。已經加入對上下文管理協議支持的還有模塊 threading、decimal 等。PEP 0343 對with語句的實現進行了描述。with語句的執行過程相似以下代碼塊:ui

清單 4. with 語句執行過程
context_manager = context_expression
exit = type(context_manager).__exit__  
value = type(context_manager).__enter__(context_manager)
exc = True   # True 表示正常執行,即使有異常也忽略;False 表示從新拋出異常,須要對異常進行處理
try:
    try:
        target = value  # 若是使用了 as 子句
        with-body     # 執行 with-body
    except:
        # 執行過程當中有異常發生
        exc = False
        # 若是 __exit__ 返回 True,則異常被忽略;若是返回 False,則從新拋出異常
        # 由外層代碼對異常進行處理
        if not exit(context_manager, *sys.exc_info()):
            raise
finally:
    # 正常退出,或者經過 statement-body 中的 break/continue/return 語句退出
    # 或者忽略異常退出
    if exc:
        exit(context_manager, None, None, None) 
    # 缺省返回 None,None 在布爾上下文中看作是 False
  1. 執行 context_expression,生成上下文管理器 context_manager
  2. 調用上下文管理器的 __enter__() 方法;若是使用了 as 子句,則將 __enter__() 方法的返回值賦值給 as 子句中的 target(s)
  3. 執行語句體 with-body
  4. 不論是否執行過程當中是否發生了異常,執行上下文管理器的 __exit__() 方法,__exit__() 方法負責執行「清理」工做,如釋放資源等。若是執行過程當中沒有出現異常,或者語句體中執行了語句 break/continue/return,則以 None 做爲參數調用 __exit__(None, None, None) ;若是執行過程當中出現異常,則使用 sys.exc_info 獲得的異常信息爲參數調用 __exit__(exc_type, exc_value, exc_traceback)
  5. 出現異常時,若是 __exit__(type, value, traceback) 返回 False,則會從新拋出異常,讓with 以外的語句邏輯來處理異常,這也是通用作法;若是返回 True,則忽略異常,再也不對異常進行處理

4. 自定義上下文管理器

開發人員能夠自定義支持上下文管理協議的類。自定義的上下文管理器要實現上下文管理協議所須要的 __enter__() 和 __exit__() 兩個方法:編碼

  • context_manager.__enter__() :進入上下文管理器的運行時上下文,在語句體執行前調用。若是指定了 as 子句的話,with 語句將該方法的返回值賦值給 as 子句中的 target。
  • context_manager.__exit__(exc_type, exc_value, exc_traceback) :退出與上下文管理器相關的運行時上下文,返回一個布爾值表示是否對發生的異常進行處理。參數表示引發退出操做的異常,若是退出時沒有發生異常,則3個參數都爲None。若是發生異常,返回True 表示不處理異常,不然會在退出該方法後從新拋出異常以由 with 語句以外的代碼邏輯進行處理。若是該方法內部產生異常,則會取代由 statement-body 中語句產生的異常。要處理異常時,不要顯示從新拋出異常,即不能從新拋出經過參數傳遞進來的異常,只須要將返回值設置爲 False 就能夠了。以後,上下文管理代碼會檢測是否 __exit__() 失敗來處理異常

下面經過一個簡單的示例來演示如何構建自定義的上下文管理器。注意,上下文管理器必須同時提供 __enter__() 和 __exit__() 方法的定義,缺乏任何一個都會致使 AttributeError;with 語句會先檢查是否提供了 __exit__() 方法,而後檢查是否認義了 __enter__() 方法。spa

假設有一個資源 DummyResource,這種資源須要在訪問前先分配,使用完後再釋放掉;分配操做能夠放到 __enter__() 方法中,釋放操做能夠放到 __exit__() 方法中。簡單起見,這裏只經過打印語句來代表當前的操做,並無實際的資源分配與釋放。線程

清單 5. 自定義支持 with 語句的對象
class DummyResource:
def __init__(self, tag):
        self.tag = tag
        print 'Resource [%s]' % tag
    def __enter__(self):
        print '[Enter %s]: Allocate resource.' % self.tag
        return self   # 能夠返回不一樣的對象
    def __exit__(self, exc_type, exc_value, exc_tb):
        print '[Exit %s]: Free resource.' % self.tag
        if exc_tb is None:
            print '[Exit %s]: Exited without exception.' % self.tag
        else:
            print '[Exit %s]: Exited with exception raised.' % self.tag
            return False   # 能夠省略,缺省的None也是被看作是False

 

DummyResource 中的 __enter__() 返回的是自身的引用,這個引用能夠賦值給 as 子句中的 target 變量;返回值的類型能夠根據實際須要設置爲不一樣的類型,沒必要是上下文管理器對象自己。

__exit__() 方法中對變量 exc_tb 進行檢測,若是不爲 None,表示發生了異常,返回 False 表示須要由外部代碼邏輯對異常進行處理;注意到若是沒有發生異常,缺省的返回值爲 None,在布爾環境中也是被看作 False,可是因爲沒有異常發生,__exit__() 的三個參數都爲 None,上下文管理代碼能夠檢測這種狀況,作正常處理。

下面在 with 語句中訪問 DummyResource :

清單 6. 使用自定義的支持 with 語句的對象
1 with DummyResource('Normal'):
2     print '[with-body] Run without exceptions.'
3  
4 with DummyResource('With-Exception'):
5     print '[with-body] Run with exception.'
6     raise Exception
7     print '[with-body] Run with exception. Failed to finish statement-body!'

 

第1個 with 語句的執行結果以下:
清單 7. with 語句1執行結果
1 Resource [Normal]
2 [Enter Normal]: Allocate resource.
3 [with-body] Run without exceptions.
4 [Exit Normal]: Free resource.
5 [Exit Normal]: Exited without exception.

 

能夠看到,正常執行時會先執行完語句體 with-body,而後執行 __exit__() 方法釋放資源。

第2個 with 語句的執行結果以下:

清單 8. with 語句2執行結果
 1 Resource [With-Exception]
 2 [Enter With-Exception]: Allocate resource.
 3 [with-body] Run with exception.
 4 [Exit With-Exception]: Free resource.
 5 [Exit With-Exception]: Exited with exception raised.
 6  
 7 Traceback (most recent call last):
 8   File "G:/demo", line 20, in <module>
 9    raise Exception
10 Exception

 

能夠看到,with-body 中發生異常時with-body 並無執行完,但資源會保證被釋放掉,同時產生的異常由 with 語句以外的代碼邏輯來捕獲處理。

能夠自定義上下文管理器來對軟件系統中的資源進行管理,好比數據庫鏈接、共享資源的訪問控制等。Python 在線文檔 Writing Context Managers 提供了一個針對數據庫鏈接進行管理的上下文管理器的簡單範例。

5. contextlib 模塊

contextlib 模塊提供了3個對象:裝飾器 contextmanager、函數 nested 和上下文管理器 closing。使用這些對象,能夠對已有的生成器函數或者對象進行包裝,加入對上下文管理協議的支持,避免了專門編寫上下文管理器來支持 with 語句。

裝飾器 contextmanager

contextmanager 用於對生成器函數進行裝飾,生成器函數被裝飾之後,返回的是一個上下文管理器,其 __enter__() 和 __exit__() 方法由 contextmanager 負責提供,而再也不是以前的迭代子。被裝飾的生成器函數只能產生一個值,不然會致使異常 RuntimeError;產生的值會賦值給 as 子句中的 target,若是使用了 as 子句的話。下面看一個簡單的例子。

清單 9. 裝飾器 contextmanager 使用示例
 1 from contextlib import contextmanager
 2  
 3 @contextmanager
 4 def demo():
 5     print '[Allocate resources]'
 6     print 'Code before yield-statement executes in __enter__'
 7     yield '*** contextmanager demo ***'
 8     print 'Code after yield-statement executes in __exit__'
 9     print '[Free resources]'
10  
11 with demo() as value:
12     print 'Assigned Value: %s' % value

 

結果輸出以下:

清單 10. contextmanager 使用示例執行結果
1 [Allocate resources]
2 Code before yield-statement executes in __enter__
3 Assigned Value: *** contextmanager demo ***
4 Code after yield-statement executes in __exit__
5 [Free resources]

 

能夠看到,生成器函數中 yield 以前的語句在 __enter__() 方法中執行,yield 以後的語句在 __exit__() 中執行,而 yield 產生的值賦給了 as 子句中的 value 變量。

須要注意的是,contextmanager 只是省略了 __enter__() / __exit__() 的編寫,但並不負責實現資源的「獲取」和「清理」工做;「獲取」操做須要定義在 yield 語句以前,「清理」操做須要定義 yield 語句以後,這樣 with 語句在執行 __enter__() / __exit__() 方法時會執行這些語句以獲取/釋放資源,即生成器函數中須要實現必要的邏輯控制,包括資源訪問出現錯誤時拋出適當的異常。

函數 nested

nested 能夠將多個上下文管理器組織在一塊兒,避免使用嵌套 with 語句。

清單 11. nested 語法
1 with nested(A(), B(), C()) as (X, Y, Z):
2      # with-body code here

 

相似於:

清單 12. nested 執行過程
with A() as X:
    with B() as Y:
        with C() as Z:
             # with-body code here

 

須要注意的是,發生異常後,若是某個上下文管理器的 __exit__() 方法對異常處理返回 False,則更外層的上下文管理器不會監測到異常。

上下文管理器 closing

closing 的實現以下:

清單 13. 上下文管理 closing 實現
class closing(object):
    # help doc here
    def __init__(self, thing):
        self.thing = thing
    def __enter__(self):
        return self.thing
    def __exit__(self, *exc_info):
        self.thing.close()

 上下文管理器會將包裝的對象賦值給 as 子句的 target 變量,同時保證打開的對象在 with-body 執行完後會關閉掉。closing 上下文管理器包裝起來的對象必須提供 close() 方法的定義,不然執行時會報 AttributeError 錯誤。

清單 14. 自定義支持 closing 的對象
 1 class ClosingDemo(object):
 2     def __init__(self):
 3         self.acquire()
 4     def acquire(self):
 5         print 'Acquire resources.'
 6     def free(self):
 7         print 'Clean up any resources acquired.'
 8     def close(self):
 9         self.free()
10  
11 with closing(ClosingDemo()):
12     print 'Using resources'

 結果輸出以下:

清單 15. 自定義 closing 對象的輸出結果
1 Acquire resources.
2 Using resources
3 Clean up any resources acquired.

 closing 適用於提供了 close() 實現的對象,好比網絡鏈接、數據庫鏈接等,也能夠在自定義類時經過接口 close() 來執行所須要的資源「清理」工做。

再看個例子:

清單 16. 上下文管理器使用分析:
 1 from contextlib import contextmanager 
 2 
 3 @contextmanager 
 4 def transaction(db): 
 5     db.begin() 
 6     try: 
 7         yield db 
 8     except: 
 9         db.rollback() 
10         raise 
11     else: 
12         db.commit()
13             

 

第一眼上看去,這種實現方式更爲簡單,可是其機制更爲複雜。看一下其執行過程吧:

  1. Python解釋器識別到yield關鍵字後,def會建立一個生成器函數替代常規的函數(在類定義以外我喜歡用函數代替方法)。

  2. 裝飾器contextmanager被調用並返回一個幫助方法,這個幫助函數在被調用後會生成一個GeneratorContextManager實例。最終with表達式中的EXPR調用的是由contentmanager裝飾器返回的幫助函數。

  3. with表達式調用transaction(db),其實是調用幫助函數。幫助函數調用生成器函數,生成器函數建立一個生成器。

  4. 幫助函數將這個生成器傳遞給GeneratorContextManager,並建立一個GeneratorContextManager的實例對象做爲上下文管理器。

  5. with表達式調用實例對象的上下文管理器的__enter()__方法。

  6. __enter()__方法中會調用這個生成器的next()方法。這時候,生成器方法會執行到yield db處中止,並將db做爲next()的返回值。若是有as VAR,那麼它將會被賦值給VAR

  7. with中的BLOCK被執行。

  8. BLOCK執行結束後,調用上下文管理器的__exit()__方法。__exit()__方法會再次調用生成器的next()方法。若是發生StopIteration異常,則pass

  9. 若是沒有發生異常生成器方法將會執行db.commit(),不然會執行db.rollback()

小結

本文對 with 語句的語法和工做機理進行了介紹,並經過示例介紹瞭如何實現自定義的上下文管理器,最後介紹瞭如何使用 contextlib 模塊來簡化上下文管理器的編寫。

 

參考:

一、簡書:樂樂樂樂樂正
連接:https://www.jianshu.com/p/4aaa570616bf

二、IBM developerworks

鏈接:https://www.ibm.com/developerworks/cn/opensource/os-cn-pythonwith/

相關文章
相關標籤/搜索