Following system colour scheme - Python 增強提案 Selected dark colour scheme - Python 增強提案 Selected light colour scheme - Python 增強提案

Python 增強提案 (Python Enhancement Proposals)

PEP 343 – “with” 陳述句

作者:
Guido van Rossum, Alyssa Coghlan
狀態:
最終 (Final)
類型:
標準軌跡 (Standards Track)
建立日期:
2005年5月13日
Python 版本:
2.5
公告歷史:
2005年6月2日、2005年10月16日、2005年10月29日、2006年4月23日、2006年5月1日、2006年7月30日

目錄

摘要

本 PEP 為 Python 語言加入了一個新的陳述句“with”,以便將 try/finally 陳述句的標準用法封裝起來。

在本 PEP 中,內容管理器(context manager)提供了 __enter__()__exit__() 方法,這些方法會在進入與離開 with 陳述句區塊時被呼叫。

作者註

本 PEP 最初由 Guido 以第一人稱撰寫,隨後由 Alyssa (Nick) Coghlan 根據 python-dev 後續的討論進行了更新。所有第一人稱引用均來自 Guido 的原文。

Python 的 Alpha 釋出週期揭露了本 PEP 以及相關文件與實作中的術語問題 [13]。本 PEP 在 Python 2.5 第一個 Beta 版本釋出前後趨於穩定。

是的,動詞時態在某些地方有點混亂。我們撰寫這份 PEP 已經超過一年了,所以當初處於未來時態的事物現在已經變成過去式了 :)

簡介

在針對 PEP 340 及其他替代方案進行大量討論後,我決定撤回 PEP 340 並提出了 PEP 310 的微小變體。經過進一步討論,我加回了一個機制,允許在暫停的產生器中使用 throw() 方法來引發例外,以及使用 close() 方法來拋出新的 GeneratorExit 例外;這些新增內容最初是在 python-dev 的 [2] 中提出並獲得一致批准的。我還將關鍵字改為“with”。

在本 PEP 被接受後,由於功能重疊,以下 PEP 被拒絕:

  • PEP 310,可靠的獲取/釋放對。這是最初的 with 陳述句提案。
  • PEP 319,Python 同步/非同步區塊。其使用案例可透過提供合適的 with 陳述句控制器來由本 PEP 涵蓋:對於“同步”,我們可以使用範例 1 中的“locking”模板;對於“非同步”,我們可以使用類似的“unlocking”模板。我不認為將一個“匿名”鎖與程式碼區塊關聯起來有多重要;事實上,明確指定所使用的互斥鎖(mutex)可能更好。

PEP 340PEP 346 也與本 PEP 重疊,但在本 PEP 提交時已主動撤回。

關於本 PEP 早期版本的討論曾發生在 Python Wiki 上 [3]

動機與總結

PEP 340(匿名區塊陳述句)結合了許多強大的想法:使用產生器作為區塊模板、增加例外處理和終結處理至產生器等。除了讚譽之外,它也招致了許多反對意見,因為人們不喜歡它在底層實際上是一個(潛在的)迴圈結構這一點。這意味著在區塊陳述句中使用 break 和 continue 會中斷或繼續該區塊陳述句,即使它僅被用作非迴圈式的資源管理工具。

但最後的致命一擊是我讀到了 Raymond Chen 關於流程控制巨集的咆哮 [1]。Raymond 極具說服力地指出,在巨集中隱藏流程控制會讓你的程式碼變得難以理解,我發現他的論點同樣適用於 Python 與 C。我意識到 PEP 340 的模板可以隱藏各種控制流;例如,其範例 4(auto_retry())會捕捉例外並將區塊重複執行最多三次。

然而,在我看來,PEP 310 的 with 陳述句並**沒有**隱藏控制流:雖然 finally 套件會暫時暫停控制流,但最終控制流會恢復,就像 finally 套件根本不存在一樣。

請記住,PEP 310 大致提議了這樣的語法(“VAR =”部分是選用的):

with VAR = EXPR:
    BLOCK

這大致翻譯為:

VAR = EXPR
VAR.__enter__()
try:
    BLOCK
finally:
    VAR.__exit__()

現在考慮這個例子:

with f = open("/etc/passwd"):
    BLOCK1
BLOCK2

在這裡,就像第一行是“if True”一樣,我們知道如果 BLOCK1 完成時沒有例外,就會到達 BLOCK2;如果 BLOCK1 引發了例外或執行了非局部跳轉(break, continue 或 return),則**不會**到達 BLOCK2。with 陳述句在最後加入的魔法不會影響這一點。

(你可能會問,如果 __exit__() 方法中的 bug 導致了例外怎麼辦?那一切就完了——但這並不比其他例外情況更糟;例外的本質就是它們可能發生在**任何地方**,你只能接受這一點。即使你寫出了無 bug 的程式碼,KeyboardInterrupt 例外仍可能導致它在兩個虛擬機操作碼之間退出。)

這個論點幾乎讓我支持了 PEP 310,但我還有一個來自 PEP 340 狂熱中的想法捨不得放棄:使用產生器作為獲取與釋放鎖或開啟與關閉檔案等抽象的“模板”是一個強大的想法,這可以從該 PEP 中的範例看出。

受到 Phillip Eby 對 PEP 340 的反對提案啟發,我嘗試建立一個裝飾器,將合適的產生器轉換為具有必要 __enter__()__exit__() 方法的物件。在這裡,我遇到了一個困難:雖然對於鎖的範例並不難,但對於檔案開啟的範例卻無法做到。當時的想法是將模板定義如下:

@contextmanager
def opening(filename):
    f = open(filename)
    try:
        yield f
    finally:
        f.close()

並這樣使用:

with f = opening(filename):
    ...read data from f...

問題在於在 PEP 310 中,呼叫 EXPR 的結果直接賦值給 VAR,然後在離開 BLOCK1 時呼叫 VAR__exit__() 方法。但在這裡,VAR 明顯需要接收開啟的檔案,這意味著 __exit__() 必須是該檔案的一個方法。

雖然這可以使用代理類別來解決,但這很笨拙,並讓我意識到一個稍微不同的翻譯方式可以讓編寫所需的裝飾器變得輕而易舉:讓 VAR 接收呼叫 __enter__() 方法後的結果,並儲存 EXPR 的值,以便稍後呼叫其 __exit__() 方法。然後,裝飾器可以回傳一個封裝類別的實例,其 __enter__() 方法呼叫產生器的 next() 方法並回傳 next() 回傳的值;該封裝實例的 __exit__() 方法再次呼叫 next(),但預期它會引發 StopIteration。(詳細資訊參見下文“可選的產生器裝飾器”章節。)

所以現在最後的障礙是 PEP 310 的語法

with VAR = EXPR:
    BLOCK1

會產生誤導,因為 VAR **不會**接收 EXPR 的值。借鑒 PEP 340,邁出這一步很容易:

with EXPR as VAR:
    BLOCK1

額外的討論顯示,人們真的很喜歡能夠在產生器中“看到”例外,即使只是為了記錄它;產生器不允許 yield 其他值,因為 with 陳述句不應被用作迴圈(引發不同的例外是勉強可以接受的)。為了啟用此功能,提議為產生器新增一個新的 throw() 方法,該方法接受一到三個參數,以通常的方式(類型、值、回溯資訊)表示一個例外,並在產生器暫停處引發它。

一旦有了這個,再提議另一個產生器方法 close() 就只是小事一樁了。該方法使用一個特殊的例外 GeneratorExit 來呼叫 throw()。這會告知產生器退出,由此再提議當產生器被垃圾回收時自動呼叫 close() 也是順理成章的。

最後,我們可以允許在 try-finally 陳述句內使用 yield 陳述句,因為我們現在可以保證 finally 子句(最終)會被執行。通常關於終結處理的警告依然適用——處理程序可能會在未終結任何物件的情況下突然終止,而物件可能會因為應用程式中的循環參考或記憶體洩漏而永遠保持存活(相對於 Python 實作中的循環或洩漏,後者由 GC 處理)。

請注意,我們並不保證 finally 子句會在產生器物件變得無用後立即執行,儘管這在 CPython 中就是這樣運作的。這類似於自動關閉檔案:雖然像 CPython 這樣的參考計數實作會在最後一個參考消失時立即釋放物件,但使用其他 GC 演算法的實作並不能做出同樣的保證。這適用於 Jython、IronPython,以及可能在 Parrot 上執行的 Python。

(產生器所做變更的詳細資訊現在可在 PEP 342 中找到,而非本 PEP)

使用案例

參閱末尾附近的“範例”章節。

規範:“with” 陳述句

提議一種語法如下的新陳述句:

with EXPR as VAR:
    BLOCK

在這裡,“with”和“as”是新的關鍵字;EXPR 是任意表達式(但不是表達式列表),VAR 是單一賦值目標。它**不能**是逗號分隔的變數序列,但**可以**是**加括號的**逗號分隔變數序列。(這項限制使得未來可以將語法擴展為多個逗號分隔的資源,每個資源都有自己可選的 as 子句。)

“as VAR”部分是選用的。

上述陳述句的翻譯為:

mgr = (EXPR)
exit = type(mgr).__exit__  # Not calling it yet
value = type(mgr).__enter__(mgr)
exc = True
try:
    try:
        VAR = value  # Only if "as VAR" is present
        BLOCK
    except:
        # The exceptional case is handled here
        exc = False
        if not exit(mgr, *sys.exc_info()):
            raise
        # The exception is swallowed if exit() returns true
finally:
    # The normal and non-local-goto cases are handled here
    if exc:
        exit(mgr, None, None, None)

這裡,小寫變數(mgr, exit, value, exc)是內部變數,使用者無法存取;它們極有可能被實作為特殊的暫存器或堆疊位置。

上述翻譯的詳細資訊旨在規範確切的語意。如果未按預期找到相關方法,解釋器將按嘗試順序(__exit__, __enter__)引發 AttributeError。同樣地,如果任何呼叫引發了例外,其效果與上述程式碼中完全相同。最後,如果 BLOCK 包含 break、continue 或 return 陳述句,__exit__() 方法會以三個 None 參數被呼叫,就像 BLOCK 正常完成一樣。(亦即,這些“偽例外”不會被 __exit__() 視為例外。)

如果語法中的“as VAR”部分被省略,則翻譯中的“VAR =”部分也會被省略(但仍會呼叫 mgr.__enter__())。

呼叫 mgr.__exit__() 的約定如下。如果 finally 套件是透過 BLOCK 的正常完成,或透過非局部跳轉(BLOCK 中的 break、continue 或 return 陳述句)到達的,則 mgr.__exit__() 會以三個 None 參數呼叫。如果 finally 套件是透過 BLOCK 中引發的例外到達的,則 mgr.__exit__() 會以表示例外類型、值和回溯資訊的三個參數呼叫。

重要:如果 mgr.__exit__() 回傳“真”值,該例外會被“吞掉”。也就是說,如果它回傳“真”,執行會從 with 陳述句之後的下一行繼續,即使例外發生在 with 陳述句內部。然而,如果 with 陳述句是透過非局部跳轉(break, continue 或 return)離開的,當 mgr.__exit__() 回傳時,無論其回傳值為何,該非局部返回都會恢復。此細節的動機是為了讓 mgr.__exit__() 有可能吞掉例外,同時又不至於太容易(因為預設回傳值 None 是假的,這會導致例外被重新引發)。吞掉例外的主要使用案例是為了能夠編寫 @contextmanager 裝飾器,使得裝飾器產生器中的 try/except 區塊表現得就像該產生器主體在 with 陳述句處被內聯展開一樣。

將例外細節傳遞給 __exit__()(相對於 PEP 310 中無參數的 __exit__())的動機,是由下文的範例 3(transactional() 使用案例)所提出的。該範例中的模板必須根據是否發生例外來提交或回滾交易。與其只使用一個布林旗標來指示是否發生例外,我們傳遞了完整的例外資訊,例如為了記錄例外功能。依賴 sys.exc_info() 來獲取例外資訊的作法被拒絕;sys.exc_info() 有非常複雜的語意,並且完全有可能回傳很久以前就被捕捉到的例外資訊。有人還提議增加一個額外的布林值來區分到達 BLOCK 結尾和非局部跳轉。這因太過複雜且不必要而被拒絕;就資料庫交易回滾決策而言,非局部跳轉應被視為非例外情況。

為了促進直接操作內容管理器的 Python 程式碼中的內容鏈結,__exit__() 方法**不應**重新引發傳入的錯誤。在這種情況下,進行任何重新引發始終是 __exit__() 方法**呼叫者**的責任。

這樣一來,如果呼叫者需要判斷 __exit__() 的呼叫是否**失敗**(相對於在傳播原始錯誤之前成功清理),就可以做到。

如果 __exit__() 沒有錯誤地回傳,這可以被解釋為 __exit__() 方法本身的成功(無論原始錯誤是否將被傳播或抑制)。

然而,如果 __exit__() 將例外傳播給其呼叫者,這意味著 __exit__() **本身**失敗了。因此,除非 __exit__() 方法確實失敗了,否則應避免引發錯誤。(而允許原始錯誤繼續傳播並不算是失敗。)

過渡計畫

在 Python 2.5 中,新的語法只有在存在未來陳述句時才會被識別:

from __future__ import with_statement

這將使“with”和“as”成為關鍵字。如果沒有這個未來陳述句,使用“with”或“as”作為識別碼會導致向 stderr 發出警告。

在 Python 2.6 中,新的語法將始終被識別;“with”和“as”始終是關鍵字。

產生器裝飾器

隨著 PEP 342 的接受,可以編寫一個裝飾器,使得可以使用一個剛好 yield 一次的產生器來控制 with 陳述句。以下是該裝飾器的草圖:

class GeneratorContextManager(object):

   def __init__(self, gen):
       self.gen = gen

   def __enter__(self):
       try:
           return self.gen.next()
       except StopIteration:
           raise RuntimeError("generator didn't yield")

   def __exit__(self, type, value, traceback):
       if type is None:
           try:
               self.gen.next()
           except StopIteration:
               return
           else:
               raise RuntimeError("generator didn't stop")
       else:
           try:
               self.gen.throw(type, value, traceback)
               raise RuntimeError("generator didn't stop after throw()")
           except StopIteration:
               return True
           except:
               # only re-raise if it's *not* the exception that was
               # passed to throw(), because __exit__() must not raise
               # an exception unless __exit__() itself failed.  But
               # throw() has to raise the exception to signal
               # propagation, so this fixes the impedance mismatch
               # between the throw() protocol and the __exit__()
               # protocol.
               #
               if sys.exc_info()[1] is not value:
                   raise

def contextmanager(func):
   def helper(*args, **kwds):
       return GeneratorContextManager(func(*args, **kwds))
   return helper

此裝飾器可以如下使用:

@contextmanager
def opening(filename):
   f = open(filename) # IOError is untouched by GeneratorContext
   try:
       yield f
   finally:
       f.close() # Ditto for errors here (however unlikely)

此裝飾器的穩健實作將成為標準函式庫的一部分。

標準函式庫中的內容管理器

可以為某些物件(如檔案、通訊端和鎖)賦予 __enter__()__exit__() 方法,這樣就不必編寫:

with locking(myLock):
    BLOCK

而只需簡單地寫:

with myLock:
    BLOCK

我認為我們應該對此保持謹慎;這可能會導致如下的錯誤:

f = open(filename)
with f:
    BLOCK1
with f:
    BLOCK2

這並不會執行你所想的那樣(f 在進入 BLOCK2 之前就關閉了)。

另一方面,這種錯誤很容易診斷;例如,上述產生器內容裝飾器在第二個 with 陳述句再次呼叫 f.__enter__() 時會引發 RuntimeError。如果在已關閉的檔案物件上呼叫 __enter__,也會引發類似的錯誤。

對於 Python 2.5,以下類型已被確定為內容管理器:

- file
- thread.LockType
- threading.Lock
- threading.RLock
- threading.Condition
- threading.Semaphore
- threading.BoundedSemaphore

decimal 模組也將加入一個內容管理器,以支援在 with 陳述句主體內使用本地十進制算術上下文,並在退出 with 陳述句時自動恢復原始上下文。

標準術語

本 PEP 提議將由 __enter__()__exit__() 方法組成的協定稱為“內容管理協定”,並將實作該協定的物件稱為“內容管理器”。[4]

陳述句中 with 關鍵字之後緊接著的表達式是“內容表達式”,因為該表達式提供了關於內容管理器在陳述句主體期間所建立的執行環境的主要線索。

with 陳述句主體中的程式碼以及 as 關鍵字後的變數名稱(或多個名稱)目前並沒有特別的術語。可以使用“陳述句主體”和“目標列表”等通用術語,如果術語不夠明確,可在前面加上“with”或“with 陳述句”。

鑑於 decimal 模組的算術上下文等物件的存在,“上下文(context)”一詞不幸地具有歧義。如有必要,可以透過使用“內容管理器”來稱呼由內容表達式建立的具體物件,並使用“執行上下文”或(最好)“執行環境”來稱呼由內容管理器進行的實際狀態修改,從而使其更具體。在單純討論 with 陳述句的使用時,歧義應該不重要,因為內容表達式完整定義了對執行環境所做的變更。在討論 with 陳述句本身的運作機制以及如何實際實作內容管理器時,區分這些術語更為重要。

快取內容管理器

許多內容管理器(如檔案和基於產生器的上下文)將是單次使用的物件。一旦 __exit__() 方法被呼叫,內容管理器將不再處於可用狀態(例如檔案已被關閉,或者基礎產生器已執行完畢)。

對於每個 with 陳述句要求一個新的管理器物件,是避免多執行緒程式碼和巢狀 with 陳述句嘗試使用同一個內容管理器時出現問題的最簡單方法。所有支援重複使用的標準函式庫內容管理器都來自 threading 模組,這並非巧合——它們都已經設計好用以處理執行緒和巢狀使用所產生的問題。

這意味著,為了儲存具有特定初始化參數的內容管理器以在多個 with 陳述句中使用,通常有必要將其儲存為一個零參數的 callable,然後在每個陳述句的內容表達式中呼叫該 callable,而不是直接快取內容管理器。

當此限制不適用時,相關內容管理器的文件應該說明清楚。

已解決的問題

以下問題已由 BDFL 批准(並且在 python-dev 上沒有任何重大異議)解決。

  1. GeneratorContextManager 在基礎產生器迭代器運作異常時應該引發什麼例外?以下引用是 Guido 為此以及 PEP 342 中的產生器 close() 方法選擇 RuntimeError 的原因(來自 [8]):

    “我寧願不為此目的引入新的例外類別,因為這不是我想讓使用者去捕捉的例外:我希望它轉化為回溯資訊,讓程式設計師看到並修正程式碼。所以現在我相信它們都應該引發 RuntimeError。這有一些先例:核心 Python 程式碼在檢測到無限遞迴時、對於未初始化的物件(以及各種雜項條件)時會引發它。”

  2. 如果 with 陳述句涉及的類別中不存在相關方法,引發 AttributeError 而不是 TypeError 是可以的。抽象物件 C API 引發 TypeError 而不是 AttributeError 是歷史的偶然,而非刻意的設計決策。[11]
  3. 具有 __enter__/__exit__ 方法的物件稱為“內容管理器”,而將產生器函式轉換為內容管理器工廠的裝飾器是 contextlib.contextmanager。在 2.5 釋出週期期間有一些其他建議 [15],但沒有令人信服的理由去棄用 PEP 實作中已經使用的術語。

被拒絕的選項

有幾個月的時間,本 PEP 禁止抑制例外,以避免隱藏控制流。實作後發現這簡直是場災難,所以 Guido 恢復了這個功能。[12]

本 PEP 的另一個引起無數問題和術語爭論的方面是提供一個類似於可迭代物件的 __iter__() 方法的 __context__() 方法 [5] [7] [9]。解釋它是什麼、為什麼存在以及它該如何運作所帶來的持續問題 [10] [12],最終導致 Guido 直接抹殺了這個概念 [14](大家也因此感到非常高興!)。

直接使用 PEP 342 產生器 API 來定義 with 陳述句的想法也被稍微考慮過 [6],但很快就因為這會讓編寫非產生器基底的內容管理器變得太困難而被摒棄。

範例

基於產生器的範例依賴於 PEP 342。此外,有些範例在實務上是不必要的,因為合適的物件(如 threading.RLock)能夠直接用於 with 陳述句中。

範例內容名稱中使用的時態並非隨意的。當名稱指向一個在 __enter__ 方法中完成並在 __exit__ 方法中撤銷的動作時,使用過去式(“-ed”)。當名稱指向一個將在 __exit__ 方法中完成的動作時,使用進行式(“-ing”)。

  1. 一個確保在區塊開頭獲取的鎖在離開區塊時被釋放的模板:
    @contextmanager
    def locked(lock):
        lock.acquire()
        try:
            yield
        finally:
            lock.release()
    

    使用方式如下:

    with locked(myLock):
        # Code here executes with myLock held.  The lock is
        # guaranteed to be released when the block is left (even
        # if via return or by an uncaught exception).
    
  2. 一個用於開啟檔案的模板,確保在離開區塊時檔案被關閉:
    @contextmanager
    def opened(filename, mode="r"):
        f = open(filename, mode)
        try:
            yield f
        finally:
            f.close()
    

    使用方式如下:

    with opened("/etc/passwd") as f:
        for line in f:
            print line.rstrip()
    
  3. 一個用於提交或回滾資料庫交易的模板:
    @contextmanager
    def transaction(db):
        db.begin()
        try:
            yield None
        except:
            db.rollback()
            raise
        else:
            db.commit()
    
  4. 不使用產生器的範例 1 重寫版:
    class locked:
       def __init__(self, lock):
           self.lock = lock
       def __enter__(self):
           self.lock.acquire()
       def __exit__(self, type, value, tb):
           self.lock.release()
    

    (此範例很容易修改以實作其他相對無狀態的範例;它顯示出如果不需要保留特殊狀態,很容易避免使用產生器。)

  5. 暫時重新導向 stdout:
    @contextmanager
    def stdout_redirected(new_stdout):
        save_stdout = sys.stdout
        sys.stdout = new_stdout
        try:
            yield None
        finally:
            sys.stdout = save_stdout
    

    使用方式如下:

    with opened(filename, "w") as f:
        with stdout_redirected(f):
            print "Hello world"
    

    當然,這不是執行緒安全的,但手動執行同樣的操作也不是。在單執行緒程式中(例如在腳本中),這是一種常見的作法。

  6. 一個 opened() 的變體,也回傳一個錯誤條件:
    @contextmanager
    def opened_w_error(filename, mode="r"):
        try:
            f = open(filename, mode)
        except IOError, err:
            yield None, err
        else:
            try:
                yield f, None
            finally:
                f.close()
    

    使用方式如下:

    with opened_w_error("/etc/passwd", "a") as (f, err):
        if err:
            print "IOError:", err
        else:
            f.write("guido::0:0::/:/bin/sh\n")
    
  7. 另一個有用的範例是封鎖訊號的操作。使用方式可能是這樣的:
    import signal
    
    with signal.blocked():
        # code executed without worrying about signals
    

    一個可選參數可以是待封鎖的訊號列表;預設情況下,所有訊號都被封鎖。實作留給讀者作為練習。

  8. 此功能的另一個用途是 Decimal 上下文。這是一個簡單的範例,改編自 Michael Chermside 發佈的內容:
    import decimal
    
    @contextmanager
    def extra_precision(places=2):
        c = decimal.getcontext()
        saved_prec = c.prec
        c.prec += places
        try:
            yield None
        finally:
            c.prec = saved_prec
    

    範例用法(改編自 Python 函式庫參考手冊):

    def sin(x):
        "Return the sine of x as measured in radians."
        with extra_precision():
            i, lasts, s, fact, num, sign = 1, 0, x, 1, x, 1
            while s != lasts:
                lasts = s
                i += 2
                fact *= i * (i-1)
                num *= x * x
                sign *= -1
                s += num / fact * sign
        # The "+s" rounds back to the original precision,
        # so this must be outside the with-statement:
        return +s
    
  9. 這是一個用於 decimal 模組的簡單內容管理器:
    @contextmanager
    def localcontext(ctx=None):
        """Set a new local decimal context for the block"""
        # Default to using the current context
        if ctx is None:
            ctx = getcontext()
        # We set the thread context to a copy of this context
        # to ensure that changes within the block are kept
        # local to the block.
        newctx = ctx.copy()
        oldctx = decimal.getcontext()
        decimal.setcontext(newctx)
        try:
            yield newctx
        finally:
            # Always restore the original context
            decimal.setcontext(oldctx)
    

    範例用法:

    from decimal import localcontext, ExtendedContext
    
    def sin(x):
        with localcontext() as ctx:
            ctx.prec += 2
            # Rest of sin calculation algorithm
            # uses a precision 2 greater than normal
        return +s # Convert result to normal precision
    
    def sin(x):
        with localcontext(ExtendedContext):
            # Rest of sin calculation algorithm
            # uses the Extended Context from the
            # General Decimal Arithmetic Specification
        return +s # Convert result to normal context
    
  10. 一個通用的“物件關閉”內容管理器:
    class closing(object):
        def __init__(self, obj):
            self.obj = obj
        def __enter__(self):
            return self.obj
        def __exit__(self, *exc_info):
            try:
                close_it = self.obj.close
            except AttributeError:
                pass
            else:
                close_it()
    

    這可用於確定性地關閉任何具有 close 方法的物件,無論是檔案、產生器還是其他東西。它甚至可以用於不保證需要關閉的物件(例如,一個接受任意可迭代物件的函式):

    # emulate opening():
    with closing(open("argument.txt")) as contradiction:
       for line in contradiction:
           print line
    
    # deterministically finalize an iterator:
    with closing(iter(data_source)) as data:
       for datum in data:
           process(datum)
    

    (Python 2.5 的 contextlib 模組包含此內容管理器的一個版本)

  11. PEP 319 提供了一個同時使用 released() 上下文來暫時釋放先前獲取的鎖的使用案例;這可以透過交換 acquire()release() 呼叫,寫得與上述 locked 內容管理器非常相似:
    class released:
      def __init__(self, lock):
          self.lock = lock
      def __enter__(self):
          self.lock.release()
      def __exit__(self, type, value, tb):
          self.lock.acquire()
    

    範例用法:

    with my_lock:
        # Operations with the lock held
        with released(my_lock):
            # Operations without the lock
            # e.g. blocking I/O
        # Lock is held again here
    
  12. 一個“巢狀”內容管理器,自動將提供的上下文從左到右巢狀,以避免過度縮排:
    @contextmanager
    def nested(*contexts):
        exits = []
        vars = []
        try:
            try:
                for context in contexts:
                    exit = context.__exit__
                    enter = context.__enter__
                    vars.append(enter())
                    exits.append(exit)
                yield vars
            except:
                exc = sys.exc_info()
            else:
                exc = (None, None, None)
        finally:
            while exits:
                exit = exits.pop()
                try:
                    exit(*exc)
                except:
                    exc = sys.exc_info()
                else:
                    exc = (None, None, None)
            if exc != (None, None, None):
                # sys.exc_info() may have been
                # changed by one of the exit methods
                # so provide explicit exception info
                raise exc[0], exc[1], exc[2]
    

    範例用法:

    with nested(a, b, c) as (x, y, z):
        # Perform operation
    

    相當於:

    with a as x:
        with b as y:
            with c as z:
                # Perform operation
    

    (Python 2.5 的 contextlib 模組包含此內容管理器的一個版本)

參考實作

本 PEP 於 2005 年 6 月 27 日在他的 EuroPython 主題演講中首次被 Guido 接受。隨後它再次被接受,並加入了 __context__ 方法。該 PEP 在 Python 2.5a1 的 Subversion 中實作。__context__() 方法在 Python 2.5b1 中被移除。

致謝

許多人為本 PEP 中的想法和概念做出了貢獻,包括 PEP 340PEP 346 致謝名單中提到的所有人。

特別感謝(順序不分先後):Paul Moore、Phillip J. Eby、Greg Ewing、Jason Orendorff、Michael Hudson、Raymond Hettinger、Walter Dörwald、Aahz、Georg Brandl、Terry Reedy、A.M. Kuchling、Brett Cannon,以及所有參與 python-dev 討論的人。

參考文獻


來源:https://github.com/python/peps/blob/main/peps/pep-0343.rst

最後修改:2025-02-01 08:59:27 GMT