引言
with 語句是從 Python 2.5 開始引入的一種與異常處理相關的功能(2.5 版本中要通過 from __future__ import with_statement 匯入後才可以使用),從 2.6 版本開始預設可用(參考 What’s new in Python 2.6?中 with 語句相關部分介紹)。with 語句適用於對資源進行訪問的場合,確保不管使用過程中是否發生異常都會執行必要的“清理”操作,釋放資源,比如檔案使用後自動關閉、執行緒中鎖的自動獲取和釋放等。
術語
要使用 with 語句,首先要明白上下文管理器這一概念。有了上下文管理器,with 語句才能工作。
下面是一組與上下文管理器和with 語句有關的概念。
上下文管理協議(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 語句的語法格式如下:
清單 1. with 語句的語法格式
1 2 |
with context_expression [as target(s)]: with-body |
這裡 context_expression 要返回一個上下文管理器物件,該物件並不賦值給 as 子句中的 target(s) ,如果指定了 as 子句的話,會將上下文管理器的 __enter__() 方法的返回值賦值給 target(s)。target(s) 可以是單個變數,或者由“()”括起來的元組(不能是僅僅由“,”分隔的變數列表,必須加“()”)。
Python 對一些內建物件進行改進,加入了對上下文管理器的支援,可以用於 with 語句中,比如可以自動關閉檔案、執行緒鎖的自動獲取和釋放等。假設要對一個檔案進行操作,使用 with 語句可以有如下程式碼:
清單 2. 使用 with 語句操作檔案物件
1 2 3 4 |
with open(r'somefileName') as somefile: for line in somefile: print line # ...more code |
這裡使用了 with 語句,不管在處理檔案過程中是否發生異常,都能保證 with 語句執行完畢後已經關閉了開啟的檔案控制程式碼。如果使用傳統的 try/finally 正規化,則要使用類似如下程式碼:
清單 3. try/finally 方式操作檔案物件
1 2 3 4 5 6 7 |
somefile = open(r'somefileName') try: for line in somefile: print line # ...more code finally: somefile.close() |
比較起來,使用 with 語句可以減少編碼量。已經加入對上下文管理協議支援的還有模組 threading、decimal 等。
PEP 0343 對 with 語句的實現進行了描述。with 語句的執行過程類似如下程式碼塊:
清單 4. with 語句執行過程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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 |
- 執行 context_expression,生成上下文管理器 context_manager
- 呼叫上下文管理器的 __enter__() 方法;如果使用了 as 子句,則將 __enter__() 方法的返回值賦值給 as 子句中的 target(s)
- 執行語句體 with-body
- 不管是否執行過程中是否發生了異常,執行上下文管理器的 __exit__() 方法,__exit__() 方法負責執行“清理”工作,如釋放資源等。如果執行過程中沒有出現異常,或者語句體中執行了語句 break/continue/return,則以 None 作為引數呼叫 __exit__(None, None, None) ;如果執行過程中出現異常,則使用 sys.exc_info 得到的異常資訊為引數呼叫 __exit__(exc_type, exc_value, exc_traceback)
- 出現異常時,如果 __exit__(type, value, traceback) 返回 False,則會重新丟擲異常,讓with 之外的語句邏輯來處理異常,這也是通用做法;如果返回 True,則忽略異常,不再對異常進行處理
自定義上下文管理器
開發人員可以自定義支援上下文管理協議的類。自定義的上下文管理器要實現上下文管理協議所需要的 __enter__() 和 __exit__() 兩個方法:
- context_manager.__enter__() :進入上下文管理器的執行時上下文,在語句體執行前呼叫。with 語句將該方法的返回值賦值給 as 子句中的 target,如果指定了 as 子句的話
- context_manager.__exit__(exc_type, exc_value, exc_traceback) :退出與上下文管理器相關的執行時上下文,返回一個布林值表示是否對發生的異常進行處理。參數列示引起退出操作的異常,如果退出時沒有發生異常,則3個引數都為None。如果發生異常,返回True 表示不處理異常,否則會在退出該方法後重新丟擲異常以由 with 語句之外的程式碼邏輯進行處理。如果該方法內部產生異常,則會取代由 statement-body 中語句產生的異常。要處理異常時,不要顯示重新丟擲異常,即不能重新丟擲通過引數傳遞進來的異常,只需要將返回值設定為 False 就可以了。之後,上下文管理程式碼會檢測是否 __exit__() 失敗來處理異常
下面通過一個簡單的示例來演示如何構建自定義的上下文管理器。注意,上下文管理器必須同時提供 __enter__() 和 __exit__() 方法的定義,缺少任何一個都會導致 AttributeError;with 語句會先檢查是否提供了 __exit__() 方法,然後檢查是否定義了 __enter__() 方法。
假設有一個資源 DummyResource,這種資源需要在訪問前先分配,使用完後再釋放掉;分配操作可以放到 __enter__() 方法中,釋放操作可以放到 __exit__() 方法中。簡單起見,這裡只通過列印語句來表明當前的操作,並沒有實際的資源分配與釋放。
清單 5. 自定義支援 with 語句的物件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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 2 3 4 5 6 7 |
with DummyResource('Normal'): print '[with-body] Run without exceptions.' with DummyResource('With-Exception'): print '[with-body] Run with exception.' raise Exception print '[with-body] Run with exception. Failed to finish statement-body!' |
第1個 with 語句的執行結果如下:
清單 7. with 語句1執行結果
1 2 3 4 5 |
Resource [Normal] [Enter Normal]: Allocate resource. [with-body] Run without exceptions. [Exit Normal]: Free resource. [Exit Normal]: Exited without exception. |
可以看到,正常執行時會先執行完語句體 with-body,然後執行 __exit__() 方法釋放資源。
第2個 with 語句的執行結果如下:
清單 8. with 語句2執行結果
可以看到,with-body 中發生異常時with-body 並沒有執行完,但資源會保證被釋放掉,同時產生的異常由 with 語句之外的程式碼邏輯來捕獲處理。
可以自定義上下文管理器來對軟體系統中的資源進行管理,比如資料庫連線、共享資源的訪問控制等。Python 線上文件 Writing Context Managers 提供了一個針對資料庫連線進行管理的上下文管理器的簡單範例。
contextlib 模組
contextlib 模組提供了3個物件:裝飾器 contextmanager、函式 nested 和上下文管理器 closing。使用這些物件,可以對已有的生成器函式或者物件進行包裝,加入對上下文管理協議的支援,避免了專門編寫上下文管理器來支援 with 語句。
1 2 3 4 5 6 7 8 9 10 11 12 |
from contextlib import contextmanager @contextmanager def demo(): print '[Allocate resources]' print 'Code before yield-statement executes in __enter__' yield '*** contextmanager demo ***' print 'Code after yield-statement executes in __exit__' print '[Free resources]' with demo() as value: print 'Assigned Value: %s' % value |
裝飾器 contextmanager
contextmanager 用於對生成器函式進行裝飾,生成器函式被裝飾以後,返回的是一個上下文管理器,其 __enter__() 和 __exit__() 方法由 contextmanager 負責提供,而不再是之前的迭代子。被裝飾的生成器函式只能產生一個值,否則會導致異常 RuntimeError;產生的值會賦值給 as 子句中的 target,如果使用了 as 子句的話。下面看一個簡單的例子。
清單 9. 裝飾器 contextmanager 使用示例
1 2 3 4 5 6 7 8 9 10 11 12 |
from contextlib import contextmanager @contextmanager def demo(): print '[Allocate resources]' print 'Code before yield-statement executes in __enter__' yield '*** contextmanager demo ***' print 'Code after yield-statement executes in __exit__' print '[Free resources]' with demo() as value: print 'Assigned Value: %s' % value |
結果輸出如下:
清單 10. contextmanager 使用示例執行結果
1 2 3 4 5 |
[Allocate resources] Code before yield-statement executes in __enter__ Assigned Value: *** contextmanager demo *** Code after yield-statement executes in __exit__ [Free resources] |
可以看到,生成器函式中 yield 之前的語句在 __enter__() 方法中執行,yield 之後的語句在 __exit__() 中執行,而 yield 產生的值賦給了 as 子句中的 value 變數。
需要注意的是,contextmanager 只是省略了 __enter__() / __exit__() 的編寫,但並不負責實現資源的“獲取”和“清理”工作;“獲取”操作需要定義在 yield 語句之前,“清理”操作需要定義 yield 語句之後,這樣 with 語句在執行 __enter__() / __exit__() 方法時會執行這些語句以獲取/釋放資源,即生成器函式中需要實現必要的邏輯控制,包括資源訪問出現錯誤時丟擲適當的異常。
函式 nested
nested 可以將多個上下文管理器組織在一起,避免使用巢狀 with 語句。
清單 11. nested 語法
1 2 |
with nested(A(), B(), C()) as (X, Y, Z): # with-body code here |
類似於:
清單 12. nested 執行過程
1 2 3 4 |
with A() as X: with B() as Y: with C() as Z: # with-body code here |
需要注意的是,發生異常後,如果某個上下文管理器的 __exit__() 方法對異常處理返回 False,則更外層的上下文管理器不會監測到異常。
上下文管理器 closing
closing 的實現如下:
清單 13. 上下文管理 closing 實現
1 2 3 4 5 6 7 8 |
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 2 3 4 5 6 7 8 9 10 11 12 |
class ClosingDemo(object): def __init__(self): self.acquire() def acquire(self): print 'Acquire resources.' def free(self): print 'Clean up any resources acquired.' def close(self): self.free() with closing(ClosingDemo()): print 'Using resources' |
結果輸出如下:
清單 15. 自定義 closing 物件的輸出結果
1 2 3 |
Acquire resources. Using resources Clean up any resources acquired. |
closing 適用於提供了 close() 實現的物件,比如網路連線、資料庫連線等,也可以在自定義類時通過介面 close() 來執行所需要的資源“清理”工作。
小結
本文對 with 語句的語法和工作機理進行了介紹,並通過示例介紹瞭如何實現自定義的上下文管理器,最後介紹瞭如何使用 contextlib 模組來簡化上下文管理器的編寫。
參考資料
學習
- Python 2.6 版本的 What’s new in Python 2.6? 對 with 語句進行了介紹。
- 參看 <<Learning Python, Third Edition>> 對異常處理、with 語句、以及生成器和裝飾器的介紹。
- 參看 <<Core Python Programming, Second Edition>> 對異常處理、with 語句、以及生成器和裝飾器的介紹。
- Python 線上文件 Context Manager Types 和 With Statement Context Managers 對上下文管理器和 with 語句進行了介紹。
- contextlib — Utilities for with-statement contexts 對 contextlib 進行了介紹,contextlib 的實現細節可以參看 Python 原始碼包下的 Lib/contextlib.py 檔案。
- PEP 0343 介紹了 with 語句的產生背景、規範以及使用。