摘要:
- Python的with語句用法以及相關的上下文管理協議
- 如何自己寫一個上下文管理器對象,如何利用
contextlib
來寫一個上下文管理器對象- 原文地址: PEP 343: The ‘with’ statement
With語句的通常用法
with語句是一個新的控制流結構, 它的基本結構如下:
with expression [as variable]:
with-block
expression
應該是可求值的,而且它的求值結果應該是一個支持上下文管理協議的對象。
這個對象可能返回一個值,這個值可以綁定到一個命名變量variable(注意variable並不是表達式結果的賦值)。
variable
能夠在with-block
語句執行前運行一些構造代碼,且在with-block
語句執行後運行一些析構代碼,甚至就算with-block
語句拋出異常了,析構代碼一樣能夠運行。
一些Python標準對象已經支持上下文管理協議,且能夠和with
一起使用,例如File
對象:
with open('/etc/passwd', 'r') as f:
for line in f:
print line
... more processing code ...
在這個語句執行後,文件對象f將會是關閉狀態,就算for循環拋出了一個一場,只是部分執行了with-block
的代碼。
threading
模塊的鎖和條件變量也支持with
語句:
lock = threading.Lock()
with lock:
# 原子操作的代碼
...
這個鎖在with-block
代碼執行之前被鎖定,而且在with-block
語句執行完以後總是被釋放。
decimal
中新的localcontext()
函數使保存和重置當前十進制環境變得很容易,它封裝了計算所需的精度和圓整度:
from decimal import Decimal, Context, localcontext
# 顯示默認的28位精度
v = Decimal('578')
print(v.sqrt())
with localcontext(Context(prec=16)):
# 這個塊中的所有代碼都會是16位的精度
# 在這個代碼塊結束以後,原始的精度將會被重置回來
print(v.sqrt())
自己動手寫一個上下文管理器
在底層實現上, with
語句還是相當複雜的,大多數人僅僅在公司中和已存在的對象一起使用with
語句, 且不需要知道這些實際細節。
所以如果你喜歡的話,可以跳過這節剩下的部分。
如果需要寫一個新的對象,且需要理解底層實現的細節,那麼就應該繼續閱讀下去。
關於上下文管理協議在高等級的角度上來解釋就是:
- 表達式是可求值的,而且應該返回一個對象叫做上下文管理器(
context manager
)。上下文管理器必須有__enter__()
和__exit__()
方法。 - 上下文管理器的
__enter__()
方法是可調用的。這個方法的返回值被賦值給VAR
。如果語句後面沒有跟隨as VAR
的話,這個值會被簡單的丟棄。 - 在
BLOCK
中的代碼將會被執行。 - 如果
BLOCK
中的代碼拋出一個異常,__exit__(type, value, traceback)
方法將會被調用,並且異常的細節將會被當作參數傳入進去,這裏的異常細節和sys.exc_info()
返回的值一樣。方法的返回值控制着異常是否會被重新拋出:任何False
的返回值將會導致異常重新拋出,而True
返回值使異常不會重新拋出。你將不會想要重新拋出異常,因爲如果你在自己的代碼中使用with
語句的話,將不會意識到有任何的出錯情況。 - 如果
BLOCK
代碼中沒有拋出異常,那麼__exit__()
方法仍然會被調用,只不過type
,value
,traceback
參數將都會是None
讓我們來看一個例子。我不會給出所有的細節代碼,僅僅會給出一些必要的代碼來表示一個支持事務功能的數據庫對象。
(對於不熟悉數據庫的人們來說,事務就是一組數據庫的改變打包到了一起,事務可以是committed
,代表着所有的更改都被寫入到了數據庫中,也可以是rolled back
, 代表所有的更改都被丟棄,數據庫沒有變化. 關於更多關於事務的信息可以參看任何數據庫相關的書籍。)
讓我們假設這裏有一個對象代表了數據庫連接,我們的目標是讓用戶能夠以下面的方式來寫代碼:
db_connection = DatabaseConnection()
with db_connection as cursor:
cursor.execute('insert into ...')
cursor.execute('delete from')
# ... more operations ...
如果with塊中的代碼被完美地執行了的話,事務應該被提交,否則如果with塊中的代碼拋出異常的話,事務應該被回滾。
我假設DatabaseConnection
對象應該有如下的基礎接口。
class DatabaseConnection:
# Database interface
def cursor(self):
"""返回一個cursor對象,而且開啓一個新的事務
"""
def commit(self):
"""提交當前事務
"""
def rollback(self):
"""回滾當前事務
"""
__enter__()
方法是很容易寫的,僅僅需要開啓一個新的事物。對於這個應用程序遊標結果對象應該是一個有用的接口,所以這個方法應該返回它,用戶能夠增加一個遊標到with語句塊中,並綁定到一個變量上。
class DatabaseConnection:
...
def __enter__(self):
# code to start a new transaction
cursor = self.cursor()
return cursor
__exit__()
方法是最複雜的,因爲這裏需要做大部分的工作。這個方法必須去檢查是否有異常發生。如果沒有異常的話,事務被提交,如果有異常的話,事務被回滾。
在如下的代碼中,異常將會放在函數的末尾,返回默認值None
. None
就是False
,所以異常就會被自動重新拋出。如果你希望的話,你可以做的更精確,在標記的位置添加一條return語句。
class DatabaseConnection:
...
def __exit__(self, type, value, tb):
if tb is None:
# No execption, so commit
self.commit()
else:
# Exception occurred, so rollback
self.rollback()
# return False
contextlib模塊
新的contextlib
模塊提供了一些有用的方法和修飾器去寫能夠和with
語句一起使用的對象。
這個叫做contextmanager
的修飾器, 它允許你去寫一個生成器函數而不是定義一個新的類。這個生成器應該精確地yield
一個值,在yield
之前的代碼將會被當作__enter__()
函數執行,且yield的返回值將會被當作__enter__()
函數的返回值,將會被綁定到with
語句後as
後的變量上(如果as後這個變量存在的話)。yield之後的代碼將會被當作__exit__()
函數來執行,任何拋出的異常將會由yield
語句來重新拋出。
我們上一節的數據庫例子可以使用這個修飾器,以如下的方式來寫:
from contextlib import contextmanager
@contextmanager
def db_transaction(connection):
cursor = connection.cursor()
try:
yield cursor
except:
connection.rollback()
raise
else:
connection.commit()
db = DatabaseConnection()
with db_transaction(db) as cursor:
...
contextlib
模塊也有一個nested(mgr1, mgr2, ...)
函數來綁定許多個上下文管理器,這樣的話你就不需要寫嵌套的with
語句了。在下面這個例子中,單個with語句獲得了線程鎖,並且開啓了一個事務。
lock = threading.Lock()
with nested (db_transaction(db), lock) as (cursor, locked):
最後,closing函數返回了一個對象,它能夠綁定到一個變量上,在函數塊的末尾,自動調用object.close()
方法
import urllib, sys
from contextlib import closing
with closing(urllib.urlopen('http://www.baidu.com')) as f:
for line in f:
sys.stdout.write(line)