Python中的上下文管理器

熊崽Kevin發表於2014-04-03

這篇文章是關於什麼的?

  • 什麼是Python中的上下文管理器
  • 怎麼使用上下文管理器
  • 如何建立自己的上下文管理器
  • 關於Python上下文庫(contextlib)

1. 上下文管理器是什麼?

舉個例子,你在寫Python程式碼的時候經常將一系列操作放在一個語句塊中:

當某條件為真 – 執行這個語句塊

當某條件為真 – 迴圈執行這個語句塊

有時候我們需要在當程式在語句塊中執行時保持某種狀態,並且在離開語句塊後結束這種狀態。

所以,事實上上下文管理器的任務是 – 程式碼塊執行前準備,程式碼塊執行後收拾。

上下文管理器是在Python2.5加入的功能,它能夠讓你的程式碼可讀性更強並且錯誤更少。接下來,讓我們來看看該如何使用。

context1

 

2. 如何使用上下文管理器?

看程式碼是最好的學習方式,來看看我們通常是如何開啟一個檔案並寫入”Hello World”?

1-2行,我們指明檔名以及開啟方式(寫入)。

第3行,開啟檔案,4-5行寫入“Hello world”,第6行關閉檔案。

這樣不就行了,為什麼還需要上下文管理器?但是我們忽略了一個很小但是很重要的細節:如果我們沒有機會到達第6行關閉檔案,那會怎樣?

舉個例子,磁碟已滿,因此我們在第4行嘗試寫入檔案時就會丟擲異常,而第6行則根本沒有機會執行。

當然,我們可以使用try-finally語句塊來進行包裝:

finally語句塊中的程式碼無論try語句塊中發生了什麼都會執行。因此可以保證檔案一定會關閉。這麼做有什麼問題麼?當然沒有,但當我們進行一些比寫入“Hello world”更復雜的事情時,try-finally語句就會變得醜陋無比。例如我們要開啟兩個檔案,一個讀一個寫,兩個檔案之間進行拷貝操作,那麼通過with語句能夠保證兩者能夠同時被關閉。

OK,讓我們把事情分解一下:

首先,建立一個名為“writer”的檔案變數。

然後,對writer執行一些操作。

最後,關閉writer。

這樣是不是優雅多了?

讓我們深入一點,“with”是一個新關鍵詞,並且總是伴隨著上下文管理器出現。“open(filename, mode)”曾經在之前的程式碼中出現。“as”是另一個關鍵詞,它指代了從“open”函式返回的內容,並且把它賦值給了一個新的變數。“writer”是一個新的變數名。

2-3行,縮排開啟一個新的程式碼塊。在這個程式碼塊中,我們能夠對writer做任意操作。這樣我們就使用了“open”上下文管理器,它保證我們的程式碼既優雅又安全。它出色的完成了try-finally的任務。

open函式既能夠當做一個簡單的函式使用,又能夠作為上下文管理器。這是因為open函式返回了一個檔案型別(file type)變數,而這個檔案型別實現了我們之前用到的write方法,但是想要作為上下文管理器還必須實現一些特殊的方法,我會在接下來的小節中介紹。

3. 自定義上下文管理器

讓我們來寫一個“open”上下文管理器。

要實現上下文管理器,必須實現兩個方法 – 一個負責進入語句塊的準備操作,另一個負責離開語句塊的善後操作。同時,我們需要兩個引數:檔名和開啟方式。

Python類包含兩個特殊的方法,分別名為:__enter__以及__exit__(雙下劃線作為字首及字尾)。

當一個物件被用作上下文管理器時:

__enter__ 方法將在進入程式碼塊前被呼叫。

__exit__ 方法則在離開程式碼塊之後被呼叫(即使在程式碼塊中遇到了異常)。

下面是上下文管理器的一個例子,它分別進入和離開程式碼塊時進行列印。

注意一些東西:

  • 沒有傳遞任何引數。
  • 在此沒有使用“as”關鍵詞。
  • 稍後我們將討論__exit__方法的引數設定。

我們如何給一個類傳遞引數?其實在任何類中,都可以使用__init__方法,在此我們將重寫它以接收兩個必要引數(filename, mode)。

當我們進入語句塊時,將會使用open函式,正如第一個例子中那樣。而當我們離開語句塊時,將關閉一切在__enter__函式中開啟的東西。

以下是我們的程式碼:

來看看有哪些變化:

3-5行,通過__init__接收了兩個引數。

7-9行,開啟檔案並返回。

12行,當離開語句塊時關閉檔案。

14-15行,模仿open使用我們自己的上下文管理器。

除此之外,還有一些需要強調的事情:

如何處理異常

我們完全忽視了語句塊內部可能出現的問題。

如果語句塊內部發生了異常,__exit__方法將被呼叫,而異常將會被重新丟擲(re-raised)。當處理檔案寫入操作時,大部分時間你肯定不希望隱藏這些異常,所以這是可以的。而對於不希望重新丟擲的異常,我們可以讓__exit__方法簡單的返回True來忽略語句塊中發生的所有異常(大部分情況下這都不是明智之舉)。

我們可以在異常發生時瞭解到更多詳細的資訊,完備的__exit__函式簽名應該是這樣的:

這樣__exit__函式就能夠拿到關於異常的所有資訊(異常型別,異常值以及異常追蹤資訊),這些資訊將幫助異常處理操作。在這裡我將不會詳細討論異常處理該如何寫,以下是一個示例,只負責丟擲SyntaxErrors異常。

4. 談一些關於上下文庫(contextlib)的內容

contextlib是一個Python模組,作用是提供更易用的上下文管理器。

contextlib.closing

假設我們有一個建立資料庫函式,它將返回一個資料庫物件,並且在使用完之後關閉相關資源(資料庫連線會話等)

我們可以像以往那樣處理或是通過上下文管理器:

contextlib.closing方法將在語句塊結束後呼叫資料庫的關閉方法。

contextlib.nested

另一個很cool的特效能夠有效地幫助我們減少巢狀:

假設我們有兩個檔案,一個讀一個寫,需要進行拷貝。

以下是不提倡的:

可以通過contextlib.nested進行簡化:

在Python2.7中這種寫法被一種新語法取代:

contextlib.contextmanager

對於Python高階玩家來說,任何能夠被yield關鍵詞分割成兩部分的函式,都能夠通過裝飾器裝飾的上下文管理器來實現。任何在yield之前的內容都可以看做在程式碼塊執行前的操作,而任何yield之後的操作都可以放在exit函式中。

這裡我舉一個執行緒鎖的例子:

鎖機制保證兩段程式碼在同時執行時不會互相干擾。例如我們有兩塊並行執行的程式碼同時寫一個檔案,那我們將得到一個混合兩份輸入的錯誤檔案。但如果我們能有一個鎖,任何想要寫檔案的程式碼都必須首先獲得這個鎖,那麼事情就好辦了。如果你想了解更多關於併發程式設計的內容,請參閱相關文獻。

下面是執行緒安全寫函式的例子:

接下來,讓我們用上下文管理器來實現,回想之前關於yield和contextlib的分析:

特別注意,這不是異常安全(exception safe)的寫法。如果你想保證異常安全,請對yield使用try語句。幸運的是threading。lock已經是一個上下文管理器了,所以我們只需要簡單地:

因為threading.lock在異常發生時會通過__exit__函式返回False,這將在yield被呼叫是被重新丟擲。這種情況下鎖將被釋放,但對於“print ‘Releasing’”的呼叫則不會被執行,除非我們重寫try-finally。

如果你希望在上下文管理器中使用“as”關鍵字,那麼就用yield返回你需要的值,它將通過as關鍵字賦值給新的變數。

全文完 :)

相關文章