Python 的上下文管理器是怎麼設計的?

豌豆花下貓發表於2021-07-14

花下貓語:最近,我在看 Python 3.10 版本的更新內容時,發現有一個關於上下文管理器的小更新,然後,突然發現上下文管理器的設計 PEP 竟然還沒人翻譯過!於是,我斷斷續續花了兩週時間,終於把這篇 PEP 翻譯出來了。如果你不瞭解什麼是 PEP,可以先檢視這篇《學習Python,怎能不懂點PEP呢?》,如果你也對翻譯 PEP 感興趣,歡迎加入 Github 上的 peps-cn 專案。

PEP原文 : https://www.python.org/dev/peps/pep-0343

PEP標題: PEP 343 -- The "with" Statement

PEP作者: Guido van Rossum, Nick Coghlan

建立日期: 2005-05-13

合入版本: 2.5

譯者 :豌豆花下貓@Python貓公眾號

PEP翻譯計劃https://github.com/chinesehuazhou/peps-cn

摘要

本 PEP 提議在 Python 中新增一種"with"語句,可以取代常規的 try/finally 語句。

在本 PEP 中,上下文管理器提供__enter__() 和 __exit__() 方法,在進入和退出 with 語句體時,這倆方法分別會被呼叫。

作者的批註

本 PEP 最初由 Guido 以第一人稱編寫,隨後由 Nick Coghlan 根據 python-dev 上的討論,做出了更新補充。所有第一人稱的內容都出自於 Guido 的原文。

Python 的 alpha 版本釋出週期暴露了本 PEP 以及相關文件和實現[14]中的術語問題。直到 Python 2.5 的第一個 beta 版本釋出時,本 PEP 才穩定下來。

是的,本文某些地方的動詞時態是混亂的。到現在為止,我們已經創作此 PEP 一年多了,所以,有些原本在未來的事情,現在已經成為過去了:)

介紹

經過對 PEP-340 及其替代方案的大量討論後,我決定撤銷 PEP-340,並提出了 PEP-310 的一個小變種。經過更多的討論後,我又新增了一種機制,可以使用 throw() 方法,在掛起的生成器中丟擲異常,或者用一個 close() 方法丟擲一個 GeneratorExitexception;這些想法最初是在 python-dev [2] 上提出的,並得到了普遍的認可。我還將關鍵字改為了“with”。

(Python貓注:PEP-340 也是 Guido 寫的,他最初用的關鍵字是“block”,後來改成了其它 PEP 提議的“with”。)

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

  • PEP-310,可靠的獲取/釋放對。這是 with 語句的原始提案。
  • PEP-319,Python 同步/非同步程式碼塊。通過提供合適的 with 語句控制器,本 PEP 可以涵蓋它的使用場景:對於'synchronize',我們可以使用示例 1 中的"locking"模板;對於'asynchronize',我們可以使用類似的"unlock"模板。我認為不必要給程式碼塊加上“匿名的”鎖;事實上,應該儘可能地使用明確的互斥鎖。

PEP-340 和 PEP-346 也與本 PEP 重疊,但當本 PEP 被提交時,它們就自行撤銷了。

關於本 PEP 早期版本的一些討論,可以在 Python Wiki[3] 上檢視。

動機與摘要

PEP-340(即匿名的 block 語句)包含了許多強大的創意:使用生成器作為程式碼塊模板、給生成器新增異常處理和終結,等等。除了讚揚之外,它還被很多人所反對,他們不喜歡它是一個(潛在的)迴圈結構。這意味著塊語句中的 break 和 continue 可以中斷或繼續塊語句,即使它原本被當作非迴圈的資源管理工具。

但是,直到我讀了 Raymond Chen 對流量控制巨集[1]的抨擊時,PEP-340 才走入了末路。Raymond 令人信服地指出,在巨集中藏有流程控制會讓你的程式碼變得難以捉摸,我覺得他的論點不僅適用於 C,同樣適用於 Python。我意識到,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 丟擲異常,或執行了非區域性的 goto (即 break、continue 或 return),那麼 BLOCK2 就不會被執行。也就是說,with 語句所加入的魔法並不會影響到這種流程邏輯。

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

這個論點幾乎讓我採納了 PEP-310,但是, PEP-340 還有一個亮點讓我不忍放棄:使用生成器作為某些抽象化行為的“模板”,例如獲取及釋放一個鎖,或者開啟及關閉一個檔案,這是一種很強大的想法,通過該 PEP 的例子就能看得出來。

受到 Phillip Eby 對 PEP-340 的反提議(counter-proposal)的啟發,我嘗試建立一個裝飾器,將合適的生成器轉換為具有必要的__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,然後 VAR 的__exit__() 方法會在 BLOCK1 退出時被呼叫。但是這裡,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() 方法,該方法以通常的方式接受 1 到 3 個引數(型別、值、回溯),表示一個異常,並在生成器掛起的地方丟擲。

一旦我們有了這個,下一步就是新增另一個生成器方法 close(),它用一個特殊的異常(即 GeneratorExit)呼叫 throw(),可以令生成器退出。有了這個,在生成器被當作垃圾回收時,可以讓程式自動呼叫 close()。

最後,我們可以允許在 try-finally 語句中使用 yield 語句,因為我們現在可以保證 finally 子句必定被執行。關於終結(finalization)的常見注意事項——程式可能會在沒有終結任何物件的情況下突然被終止,而這些物件可能會因程式的週期或記憶體洩漏而永遠存活(在 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 的正常完成或通過非區域性 goto(即 BLOCK 中的 break、continue 或 return 語句)到達,則使用三個 None 引數呼叫mgr.__exit__()。如果 finally 子句是通過 BLOCK 引發的異常到達,則使用異常的型別、值和回溯這三個引數呼叫 mgr.__exit__()。

重要:如果 mgr.__exit__() 返回“true”,則異常將被“吞滅”。也就是說,如果返回"true",即便在 with 語句內部發生了異常,也會繼續執行 with 語句之後的下一條語句。然而,如果 with 語句通過非區域性 goto (break、continue 或 return)跳出,則這個非區域性返回將被重置,不管 mgr.__exit__() 的返回值是什麼。這個細節的動機是使 mgr.__exit__() 能夠吞嚥異常,而不使異常產生影響(因為預設的返回值 None為 false,這會導致異常被重新 raise)。吞下異常的主要用途是使編寫 @contextmanager 裝飾器成為可能,這樣被裝飾的生成器中的 try/except 程式碼塊的行為就好像生成器的主體在 with-語句裡內聯展開了一樣。

之所以將異常的細節傳給__exit__(),而不用 PEP -310 中不帶引數的__exit__(),原因是考慮到下面例子 3 的 transactional()。該示例會根據是否發生異常,從而決定提交或回滾事務。我們沒有用一個 bool 標誌區分是否發生異常,而是傳了完整的異常資訊,目的是可以記錄異常日誌。依賴於 sys.exc_info() 獲取異常資訊的提議被拒絕了;因為 sys.exc_info() 有著非常複雜的語義,它返回的異常資訊完全有可能是很久之前就捕獲的。有人還提議新增一個布林值,用於區分是到達 BLOCK 結尾,還是非區域性 goto。這因為過於複雜和不必要而被拒絕;對於資料庫事務回滾,非區域性 goto 應該被認為是正常的。

為了促進 Python 程式碼中上下文的連結作用,__exit__() 方法不應該繼續 raise 傳遞給它的錯誤。在這種情況下,__exit__() 方法的呼叫者應該負責處理 raise。

這樣,如果呼叫者想知道__exit__() 是否呼叫失敗(而不是在傳出原始錯誤之前就完成清理),它就可以自己判斷。

如果__exit__() 沒有返回錯誤,那麼就可以將__exit__() 方法本身解釋為成功(不管原始錯誤是被傳播還是抑制)。

然而,如果__exit__() 向其呼叫者傳播了異常,這就意味著__exit__() 本身已經失敗。因此,__exit__() 方法應該避免引發錯誤,除非它們確實失敗了。(允許原始錯誤繼續並不是失敗。)

過渡計劃

在 Python 2.5 中,新語法需要通過 future 引入:

from __future__ import with_statement

它會引入'with'和'as'關鍵字。如果沒有匯入,使用'with'或'as'作為識別符號時,將導致報錯。

在 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

它可能跟你想的不一樣(在進入 block2 之前,f 已經關閉了)。

另一方面,這樣的錯誤很容易診斷;例如,當第二個 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 程式碼塊中所建立的執行時環境的主要線索。

目前為止, with 語句體中的程式碼和 as 關鍵字後面的變數名(一個或多個)還沒有特殊的術語。可以使用一般的術語“語句體”和“目標列表”,如果這些術語不清晰,可以使用“with”或“with statement”作為字首。

考慮到可能存在 decimal 模組的算術上下文這樣的物件,因此術語“上下文”是有歧義的。如果想要更加具體的話,可以使用術語“上下文管理器”,表示上下文表示式所建立的具體物件;使用術語“執行時上下文”或者(最好是)"執行時環境",表示上下文管理器所做出的實際狀態的變更。當簡單地討論 with 語句的用法時,歧義性無關緊要,因為上下文表示式完全定義了對執行時環境所做的更改。當討論 with 語句本身的機制以及如何實際實現上下文管理器時,這些術語的區別才是重要的。

快取上下文管理器

許多上下文管理器(例如檔案和基於生成器的上下文)都是一次性的物件。一旦__exit__() 方法被呼叫,上下文管理器將不再可用(例如:檔案已經被關閉,或者底層生成器已經完成執行)。

對於多執行緒程式碼,以及巢狀的 with 語句想要使用同一個上下文管理器,最簡單的方法是給每個 with 語句一個新的管理器物件。並非巧合的是,標準庫中所有支援重用的上下文管理器都來自 threading 模組——它們都被設計用來處理由執行緒和巢狀使用所產生的問題。

這意味著,為了儲存帶有特定初始化引數(為了用在多個 with 語句)的上下文管理器,通常需要將它儲存在一個無引數的可呼叫物件,然後在每個語句的上下文表示式中呼叫,而不是直接把上下文管理器快取起來。

如果此限制不適用,在受影響的上下文管理器的文件中,應該清楚地指出這一點。

解決的問題

以下的問題經由 BDFL 的裁決而解決(並且在 python-dev 上沒有重大的反對意見)。

1、當底層的生成器-迭代器行為異常時,GeneratorContextManager 應該引發什麼異常?下面引用的內容是 Guido 為本 PEP及 PEP-342 (見[8])中生成器的 close() 方法選擇 RuntimeError 的原因:“我不願意只是為了它而引入一個新的異常類,因為這不是我想讓人們捕獲的異常:我想讓它變成一個回溯(traceback),被程式設計師看到並且修復。因此,我認為它們都應該引發 RuntimeError。有一些引發 RuntimeError 的先例:Python 核心程式碼在檢測到無限遞迴時,遇到未初始化的物件時(以及其它各種各樣的情況)。”

2、如果在with語句所涉及的類中沒有相關的方法,則最好是丟擲AttributeError而不是TypeError。抽象物件C API引發TypeError而不是AttributeError,這只是歷史的一個偶然,而不是經過深思熟慮的設計決策[11]。

3、帶有__enter__ /__exit__方法的物件被稱為“上下文管理器”,將生成器函式轉化為上下文管理器工廠的是 contextlib.contextmanager 裝飾器。在 2.5版本釋出期間,有人提議使用其它的叫法[16],但沒有足夠令人信服的理由。

拒絕的選項

在長達幾個月的時間裡,對於是否要抑制異常(從而避免隱藏的流程控制),出現了一場令人痛苦的拉鋸戰,最終,Guido 決定要抑制異常[13]。

本 PEP 的另一個話題也引起了無休止的爭論,即是否要提供一個__context__() 方法,類似於可迭代物件的__iter__() 方法[5][7][9]。源源不斷的問題[10][13]在解釋它是什麼、為什麼是那樣、以及它是如何工作的,最終導致 Guido 完全拋棄了這個東西[15](這很讓人歡欣鼓舞!)

還有人提議直接使用 PEP-342 的生成器 API 來定義 with 語句[6],但這很快就不予考慮了,因為它會導致難以編寫不基於生成器的上下文管理器。

例子

基於生成器的示例依賴於 PEP-342。另外,有些例子是不實用的,因為標準庫中有現成的物件可以在 with 語句中直接使用,例如 threading.RLock。

例子中那些函式名所用的時態並不是隨意的。過去時態(“-ed”)的函式指的是在__enter__方法中執行,並在__exit__方法中反執行的動作。進行時態("-ing")的函式指的是準備在__exit__方法中執行的動作。

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 給出了一個用例,它也有一個 release() 上下文,能臨時釋放先前獲得的鎖;這個用例跟前文的例子 4 很相似,只是交換了 acquire() 和 release() 的呼叫:

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 模組包含了這個上下文管理器的一個版本)

參考實現

在 2005 年 6 月 27 日的 EuroPython 會議上,Guido 首次採納了這個 PEP。之後它新增了__context__方法,並被再次採納。此 PEP 在 Python 2.5 a1 子版本中實現,__context__() 方法在 Python 2.5b1 中被刪除。

致謝

許多人對這個 PEP 中的想法和概念作出了貢獻,包括在 PEP-340 和 PEP-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 討論的人。

參考連結

[1] Raymond Chen's article on hidden flow controlhttps://devblogs.microsoft.com/oldnewthing/20050106-00/?p=36783

[2] Guido suggests some generator changes that ended up in PEP 342https://mail.python.org/pipermail/python-dev/2005-May/053885.html

[3] Wiki discussion of PEP 343http://wiki.python.org/moin/WithStatement

[4] Early draft of some documentation for the with statementhttps://mail.python.org/pipermail/python-dev/2005-July/054658.html

[5] Proposal to add the with methodhttps://mail.python.org/pipermail/python-dev/2005-October/056947.html

[6] Proposal to use the PEP 342 enhanced generator API directlyhttps://mail.python.org/pipermail/python-dev/2005-October/056969.html

[7] Guido lets me (Nick Coghlan) talk him into a bad idea ?https://mail.python.org/pipermail/python-dev/2005-October/057018.html

[8] Guido raises some exception handling questionshttps://mail.python.org/pipermail/python-dev/2005-June/054064.html

[9] Guido answers some questions about the context methodhttps://mail.python.org/pipermail/python-dev/2005-October/057520.html

[10] Guido answers more questions about the context methodhttps://mail.python.org/pipermail/python-dev/2005-October/057535.html

[11] Guido says AttributeError is fine for missing special methodshttps://mail.python.org/pipermail/python-dev/2005-October/057625.html

[12] Original PEP 342 implementation patchhttp://sourceforge.net/tracker/index.php?func=detail&aid=1223381&group_id=5470&atid=305470

[13] (1, 2) Guido restores the ability to suppress exceptionshttps://mail.python.org/pipermail/python-dev/2006-February/061909.html

[14] A simple question kickstarts a thorough review of PEP 343https://mail.python.org/pipermail/python-dev/2006-April/063859.html

[15] Guido kills the context() methodhttps://mail.python.org/pipermail/python-dev/2006-April/064632.html

[16] Proposal to use 'context guard' instead of 'context manager'https://mail.python.org/pipermail/python-dev/2006-May/064676.html

版權

本文件已進入公共領域。

源文件:https://github.com/python/peps/blob/master/pep-0343.txt

相關文章