PEP 343: Python的with語句

摘要:

  1. Python的with語句用法以及相關的上下文管理協議
  2. 如何自己寫一個上下文管理器對象,如何利用contextlib來寫一個上下文管理器對象
  3. 原文地址: 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)

參考

PEP 343, The ‘with’ statement
contextlib文檔

發佈了69 篇原創文章 · 獲贊 17 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章