Python編程:輕松搞透上下文管理器(Context Manager)
前言
本文聚焦在Python的上下文管理的講解和應(yīng)用。還是通過代碼實(shí)例的方式,對(duì)照理解和學(xué)習(xí),以達(dá)到“多快好省”的理解、掌握和應(yīng)用。閑話少敘,開始——
1.何為上下文管理器
上下文管理器是一個(gè)對(duì)象,它定義了在執(zhí)行with語句時(shí)要建立的運(yùn)行時(shí)上下文。上下文管理器是為代碼塊所執(zhí)行的上下文環(huán)境自動(dòng)處理進(jìn)入和退出所需的運(yùn)行時(shí)。上下文管理器通常使用with語句調(diào)用,但也可以通過直接調(diào)用它們的方法來使用。
上下文管理器的典型用途包括保存和恢復(fù)各種全局狀態(tài),鎖定和解鎖資源,關(guān)閉打開的文件,等等。
在章節(jié)中,我們將學(xué)習(xí)如何使用Python中的上下文管理器以及如何自定義上下文管理器。
1.With語句
with語句用于上下文管理器定義的方法包裝塊的執(zhí)行。這允許封裝常見的try…except…finally使用模式以方便重用。與傳統(tǒng)的try…except…finally塊相比,with語句提供了更短且可重用的代碼。
在Python標(biāo)準(zhǔn)庫中,許多類都支持with語句。一個(gè)非常常見的例子是內(nèi)置的open()函數(shù),它提供了使用with語句處理文件對(duì)象的模式。
下面是with語句的一般語法:
我們看一個(gè)使用open()函數(shù)的例子。在當(dāng)前項(xiàng)目的files文件夾中有一個(gè)文本文件。文件名為color_names.txt,其中包含一些顏色名稱(可自行提供一些文本內(nèi)容)。我們希望通過使用open()函數(shù)和with語句打開并打印該文件中的內(nèi)容。代碼示例如下:
運(yùn)行程序輸出結(jié)果如下
在上面清單中,所看到是with語句的一個(gè)常見用例。我們使用open()函數(shù)打開給定的路徑(path)上的文件,且open()函數(shù)以只讀模式返回文件對(duì)象。然后代碼中使用這個(gè)文件對(duì)象讀取并通過代碼:print(file.read())將其內(nèi)容打印輸出。
上面示例是上下文管理器的一個(gè)典型用法。為了更好地理解和應(yīng)用上下文管理器,我們還得繼續(xù)往下看。
3. 上下文管理器協(xié)議
上下文管理器協(xié)議(Context Manager Protocol),說白了就是上下文管理器的處理機(jī)制,或說預(yù)定的規(guī)約標(biāo)準(zhǔn)。這部分內(nèi)容也可查看這里:Python核心協(xié)議。為了閱讀的獨(dú)立性,這里也再說一說。
Python的with語句支持由上下文管理器定義的運(yùn)行時(shí)上下文的概念。這是通過一對(duì)方法實(shí)現(xiàn)的,它們允許用戶定義的類定義運(yùn)行時(shí)上下文,該上下文在語句體執(zhí)行之前進(jìn)入,并在語句結(jié)束時(shí)退出。
前所提到的這些方法稱為上下文管理器協(xié)議。來具體看一下這兩個(gè)方法:
1)__enter__(self)
該方法由with語句調(diào)用,以進(jìn)入與當(dāng)前對(duì)象相關(guān)的運(yùn)行時(shí)上下文。with語句將此方法的返回值綁定到語句的as子句中指定的目標(biāo)(如果有的話)。
上例中返回的上下文管理器的是文件對(duì)象。在背后,文件對(duì)象從__enter__()返回其本身,以允許open()被用作with語句中的上下文表達(dá)式。
2)__exit__(self, exc_type, exc_value, traceback):
當(dāng)執(zhí)行離開with代碼塊時(shí)調(diào)用此方法。它退出與此對(duì)象相關(guān)的運(yùn)行時(shí)上下文。參數(shù)描述了導(dǎo)致退出上下文的異常信息。如果沒有異常而退出上下文,那么所有三個(gè)參數(shù)都將為None。
如果提供了異常,并且希望該方法抑制該異常(即,阻止它被傳播),那么它應(yīng)該返回一個(gè)True值。否則,異常將在退出此方法時(shí)正常處理。__exit__()方法返回一個(gè)布爾值,可以是True或False。
使用上下文管理器協(xié)議中的方法執(zhí)行with語句的過程如下:
- 計(jì)算上下文表達(dá)式(EXPRESSION)以獲得上下文管理器。
- 加載上下文管理器的__enter__()以供隨后使用。
- 加載上下文管理器的__exit__()以供隨后使用。
- 調(diào)用上下文管理器的__enter__()方法。
- 如果在with語句中包含了一個(gè)TARGET,則會(huì)將__enter__()的返回值賦給它。
- 執(zhí)行套件(with語句作用域中的代碼塊)。
- 調(diào)用上下文管理器的__exit__()方法。如果異常導(dǎo)致套件退出,則其類型、值和回溯將作為參數(shù)傳遞給__exit__()。否則,將提供三個(gè)None參數(shù)。
如果套件因異常以外的任何原因退出,則會(huì)忽略__exit__()的返回值,并在所執(zhí)行退出類型的正常位置繼續(xù)執(zhí)行后續(xù)代碼(若有)。
4. 類形式上下文管理器
現(xiàn)在我們了解了上下文管理器協(xié)議背后的基本思想,讓我們在一個(gè)類中實(shí)現(xiàn)它。這個(gè)類將是我們的上下文管理器,并稍后在with語句中使用它。
定義的上下文管理器類參考示例清單如下:
我們的CustomContextManager類實(shí)現(xiàn)了成為上下文管理器的必要方法:__enter__和__exit__。
在__init__方法中,它定義了三個(gè)實(shí)例變量來存儲(chǔ)路徑、模式和文件對(duì)象。
在__enter__方法中,它使用內(nèi)置的open()函數(shù)打開指定路徑中的文件。由于open()函數(shù)返回file對(duì)象,我們將其賦值給self.file屬性。
在__exit__方法中,我們將文件關(guān)閉:self.file.close()。
__exit__方法接受三個(gè)參數(shù),它們是上下文管理器協(xié)議所需要的。
現(xiàn)在我們可以在with語句中使用自定義上下文管理器。
使用自定義的類上下文管理器的示例(和我們前面的示例雷同):
運(yùn)行輸出結(jié)果這里不再贅述。簡單解釋一下代碼。
上面清單中,在with語句中使用CustomContexManager類,通過它來讀取文件內(nèi)容并打印出來。下面是這個(gè)自定義上下文管理器幕后的故事:
1)在with行,調(diào)用類CustomContextManager的方_enter__法
2) __enter__方法打開文件并返回它。
3)我們將打開的文件簡單地命名為file。
4)在with語句塊中,讀取文件內(nèi)容并將其打印出來。
5)with語句自動(dòng)調(diào)用__exit__方法。
6)__exit__方法關(guān)閉文件。
我們再來定義另一個(gè)上下文管理器類。這次我們想打印指定文件夾中的文件列表。
參考實(shí)現(xiàn)的代碼清單如下:
在代碼清單中,我們定義了一個(gè)新的上下文管理器。這個(gè)類的名字是ContentList。為什么它是一個(gè)上下文管理器?因?yàn)樗鼘?shí)現(xiàn)了上下文管理器協(xié)議(__enter__和__exit__方法)。
我們將目錄路徑作為類構(gòu)造函數(shù)__init__方法中的參數(shù)。
在__enter__方法中,只需調(diào)用os模塊中的listdir()方法,就可以獲得該目錄中的內(nèi)容列表:os.listdir(self.directory)。然后返回這個(gè)列表。請(qǐng)注意,在這個(gè)上下文管理器中我們的__enter__方法返回一個(gè)列表。
在__exit__方法中,我們檢查是否存在任何錯(cuò)誤。如果我們的上下文管理器中有錯(cuò)誤,exc_type、exc_val、exc_tb參數(shù)值將不會(huì)為None。因此,我們檢查exc_type是否為None以打印錯(cuò)誤文本。
在with語句中使用該上下文管理器。由于它返回一個(gè)列表對(duì)象,我們只需將返回值賦值給directory_list變量。在with語句的主體中,我們打印這個(gè)列表。運(yùn)行程序后在輸出中,可以看到項(xiàng)目目錄中的內(nèi)容列表。記住,"."表示當(dāng)前目錄,在我們的例子中是項(xiàng)目根目錄(由于項(xiàng)目環(huán)境不同,輸出內(nèi)容可能也不一樣)。
6. 函數(shù)形式上下文管理器
前文中,我們學(xué)習(xí)了如何使用類語法定義上下文管理器。但是有點(diǎn)繁瑣和冗長。因?yàn)樾枰鞔_地實(shí)現(xiàn)__enter__和exit__方法,還需要處理可能的異常。所以希望Python中能有在創(chuàng)建上下文管理器更好的方法:基于函數(shù)的上下文管理器。
其實(shí)函數(shù)上下文管理器是使用生成器和contextlib.contextmanager裝飾器的特殊函數(shù)。 contextlib.contextmanager裝飾器負(fù)責(zé)實(shí)現(xiàn)上下文管理器協(xié)議。
下面就來定義一個(gè)函數(shù)型上下文管理器。
運(yùn)行程序輸出結(jié)果類似如下:
在上面代碼中,我們定義了一個(gè)作為上下文管理器的自定義函數(shù)。contextmanager裝飾器將常規(guī)函數(shù)轉(zhuǎn)換為全堆棧上下文管理器(自動(dòng)實(shí)現(xiàn)上下文管理器的協(xié)議)。如果你為函數(shù)提供了@contextmanager裝飾器,就不需要擔(dān)心實(shí)現(xiàn)__enter__和__exit__函數(shù)。
代碼中的yield語句在基于類的上下文管理器中的__enter__方法中充當(dāng)返回語句。由于我們使用了yield語句,故此,這個(gè)基于函數(shù)的上下文管理器也是生成器函數(shù)。
再來定義一個(gè)新的上下文管理器。這一次,它將以寫的模式打開一個(gè)文件并添加一些文本。示例如下:
代碼清單
在清單中,我們定義了一個(gè)基于函數(shù)的上下文管理器。在try塊中,它嘗試打開指定路徑中的文件,并指定了文件的默認(rèn)編碼集。如果它成功地打開它,那么它將生成(返回)file_object。在finally塊中,我們檢查是否有一個(gè)file_object要關(guān)閉。如果file_object不是None,則關(guān)閉file_object。
在with語句中,我們用文件名funBasedContextManagers.txt調(diào)用上下文管理器。上下文管理器以寫模式打開該文件并返回文件對(duì)象,我們將其簡單命名為file。接著在這個(gè)文件中寫入一些文本。記住,如果這樣的文件不存在,'w'模式將創(chuàng)建一個(gè)空文件。
運(yùn)行上面程序,若文件不存在,則生成相應(yīng)名稱的文件并保持寫入的內(nèi)容。若文件存在,則這種寫入文件是每次情況源文件再寫入內(nèi)容的,這一點(diǎn)操作請(qǐng)注意。
像這種處理“收尾”工作的,使用上下文管理器特別方便,尤其涉及到數(shù)據(jù)庫操作方面,比如可以自己包裝一個(gè)來完成自動(dòng)的關(guān)閉連接等。
本文小結(jié)
本期我們介紹了上下文管理器的相關(guān)編程內(nèi)容,諸如何為上下文管理器、上下文管理器協(xié)議、自定義的類形式上下文管理器以及函數(shù)型上下文管理器等。相關(guān)內(nèi)容大都配合可實(shí)戰(zhàn)的代碼進(jìn)行了演示和解說。要想提高編程技能,多動(dòng)手敲代碼是必不可少的要求。