Python上下文管理器詳細使用教程
with語句會設(shè)置一個臨時的上下文,交給上下文管理器對象控制,并且負責(zé)清理上下問題。
這樣做能避免錯誤并減少樣板代碼,因此API能更安全,更易使用。除了自動關(guān)閉文件之外,with塊還有很多用途。
上下文管理器和with塊
上下文管理器對象目的是管理with語句,就像迭代器的存在是為了管理for語句一樣。
with語句的目的是簡化try/finnally模式。
這種模式用于保證一段代碼運行完畢后執(zhí)行某項操作,即便那段代碼是由于異常、return語句、sys.exit()調(diào)用而終止的,也都會執(zhí)行指定的finally操作。finally子句中通常存放用于釋放重要資源,或者還原臨時變更的狀態(tài)。
上下文管理器協(xié)議包含__enter__和__exit__兩個方法。
with語句開始運行時,會在上下文管理對象上調(diào)用__enter__方法;with語句運行結(jié)束之后,會在上下文管理對象上調(diào)用__exit__方法,以扮演finally子句的角色。
示例,把文件對象當(dāng)做上下文管理器對象使用。
with open('cafe.txt') as fp: src = fp.read(60) print(fp) # fp變量依舊可以用 print(fp.closed, fp.encoding) # 讀取fp對象的屬性 print(fp.read()) # 但是執(zhí)行fp的IO操作會異常
打印
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp936'>
True cp936
Traceback (most recent call last):
File "C:/Users/lijiachang/PycharmProjects/collect_demo/test2.py", line 8, in <module>
print(fp.read())
ValueError: I/O operation on closed file.
知識點:
- fp變量在上下文管理器之外,依舊存在,可以讀取fp對象屬性。因為with塊于函數(shù)和模塊不同,沒有定義新的作用域。
- 但是 不能在fp上再執(zhí)行IO操作,因為在with塊的末尾,已經(jīng)調(diào)用了TextIOWrapper__exit__方法把文件關(guān)閉了。
執(zhí)行with后面的表達式的結(jié)果是上下文管理器對象,不過,把值綁定到目標(biāo)變量(as后的變量)是在上下文管理器對象上調(diào)用__enter__方法的結(jié)果。
不管控制流程以哪種方式退出with塊,都會在上下文管理器對象上調(diào)用__exit__方法,而不是在__enter__方法返回的對象上調(diào)用。
with語句的as子句是可選的。對于像open這樣的函數(shù)來說,必須加上as子句,以便獲取文件的對象引用。不過一些上下文管理器對象會返回None,因為沒有什么有用的對象給用戶提供。
示例,實現(xiàn)一個LookingGlass類,上下文管理器
class LookingGlass: """鏡子:看到的字是反的""" def __enter__(self): import sys self.original_write = sys.stdout.write # 把原始的[屏幕打印輸出]函數(shù)保存到一個實例屬性中,供以后使用 sys.stdout.write = self.reverse_write # 猴子補?。禾鎿Q成自己的方法實現(xiàn) return "ABCD" def reverse_write(self, text): self.original_write(text[::-1]) # 調(diào)用原始的屏幕打印,但是把內(nèi)容反轉(zhuǎn) def __exit__(self, exc_type, exc_val, exc_tb): import sys sys.stdout.write = self.original_write # 還原成原始的函數(shù)功能 if exc_type is ZeroDivisionError: print('Do not divide by Zero!') return True # 告訴解釋器,異常已經(jīng)處理 # 其他的情況返回None,交給Python拋出異常 with LookingGlass() as what: print('lijiachang') print(what) print(what) print('back to normal')
打印
gnahcaijil
DCBA
ABCD
back to normal
知識點:
- sys.stdout.write 是標(biāo)準(zhǔn)屏幕打印輸出。要注意如果暫時緩存其他對象改變功能,記得最后還原成原來的版本。
- Python調(diào)用__enter__方法時,除了self之外不會傳入其他參數(shù)
- 如果一切正常,Python調(diào)用__exit__方法時,傳入的是None,None,None;如果拋出了異常,這三個參數(shù)是異常數(shù)據(jù)。如下:
exec_type: 異常類名稱。如ZeroDivisionError
exc_value: 異常實例。有時會有參數(shù)傳遞給異常構(gòu)造方法,例如錯誤信息,這些參數(shù)使用exc_value.args獲取
traceback: traceback對象。
補充,在try/finally語句的finally塊中調(diào)用sys.exc_info()得到的就是__exit__接收的這三個參數(shù)。
- 在__exit__中返回True是告訴解釋器,異常已經(jīng)處理了。如果返回True之外的值,比如默認的None,with塊中的異常會向上冒泡,讓Python來拋出。
In [44]: manager = LookingGlass()
In [45]: manager
Out[45]: <__main__.LookingGlass at 0xac6a970>
In [46]: m = manager.__enter__()
In [47]: m == "ABCD"
Out[47]: eurT
In [49]: m
Out[49]: 'DCBA'
In [50]: manager
Out[50]: >079a6cax0 ta ssalGgnikooL.__niam__<
In [54]: manager.__exit__(None, None, None)
In [55]: m
Out[55]: 'ABCD'
可以看到在調(diào)用__enter__之后,所有的標(biāo)準(zhǔn)打印輸出,都會反轉(zhuǎn)。因為stdout的所有輸出都經(jīng)過了__enter__方法中打補丁的reverse_write方法實現(xiàn)。
contextlib模塊
Python標(biāo)準(zhǔn)庫文檔中的contextlib模塊,提供了自定義上下文管理器的一些函數(shù)
- closing :如果對象提供了close()方法,但沒有實現(xiàn)__enter__/__exit__方法,可以使用這個函數(shù)來構(gòu)建上下文管理器。
- suppress : 構(gòu)建臨時忽略指定異常的上下文。
- @contextmanager :這個裝飾器可以把簡單的生成器變?yōu)樯舷挛墓芾砥鳎@樣就不需要創(chuàng)建類來實現(xiàn)管理器協(xié)議了。
- ContextDecorate :這是個基類,用于編寫類可以繼承他,用于定義基于類的上下文管理器。也可以用于裝飾器函數(shù),在受管理的上下文中運行整個函數(shù)。
- ExitStack : 這個上下文管理器能進入多個上下文管理器。with塊結(jié)束時,按照后進先出的順序調(diào)用棧中各個上下文管理器的__exit__方法。如果事先不知道 with塊要進入多少個上下文管理器,可以使用這個類。
使用最廣泛的還是@contextmanager裝飾器。要注意,這個裝飾器與迭代無關(guān),卻要使用yeild關(guān)鍵字。
@contextmanager 裝飾器
使用@contextmanager裝飾器呢個減少創(chuàng)建上下文管理器的代碼量,因為不用編寫一個完整的類,不用定義__enter__和__exit__方法,只需要一個實現(xiàn)yeild語句的生成器,生成想讓__enter__方法返回的值。
其中yeild語句的作用是把函數(shù)的定義體分為兩部分:
yeild語句前面的代碼在with塊開始時(即解釋器調(diào)用__enter__方法時)執(zhí)行。
yeild語句后面的代碼在with塊結(jié)束時(即調(diào)用__exit__方法時)執(zhí)行。
示例,使用生成器實現(xiàn)上下文管理器
import contextlib @contextlib.contextmanager def looking_glass(): import sys original_write = sys.stdout.write # 把原始的[屏幕打印輸出]函數(shù)保存到一個實例屬性中,供以后使用 def reverse_write(text): original_write(text[::-1]) sys.stdout.write = reverse_write # 猴子補丁:替換成自己的方法實現(xiàn) yield "ABCD" # 這個值會綁定到as后的變量上 sys.stdout.write = original_write with looking_glass() as what: print('lijiachang') print(what) print(what) print('back to normal')
知識點 :
- yield 后面的值會綁定到with語句中as子句的目標(biāo)變量上,執(zhí)行with塊中的代碼,這個函數(shù)會在這里暫停。
- 控制權(quán)一旦調(diào)成with塊,就會繼續(xù)執(zhí)行yeild語句后的代碼
@contextmanager 原理和注意事項
其實,contextlib.contextmanager裝飾器會把函數(shù)包裝實現(xiàn)成__enter__和__exit__方法的類。(ps:類的名字叫_GeneratorContextManager)
這個類的__enter__方法有如下作用:
- 調(diào)用生成器函數(shù),保存生成器對象(這里把他稱為gen)。
- 調(diào)用next(gen),執(zhí)行到y(tǒng)eild關(guān)鍵字所在的位置。
- 返回next(gen)產(chǎn)出的值,把產(chǎn)出的值綁定到with/as語句的目標(biāo)變量上。
with塊終止時,__exit__方法會做以下事情:
- 檢查有沒有異常傳給exc_type:
- 如果有,就調(diào)用gen.throw(exception), 在生成器函數(shù)定義體中包含yeild關(guān)鍵字的那一行拋出異常。
- 如果沒有異常,再次調(diào)用next(gen),繼續(xù)執(zhí)行定義體中yeild語句之后的代碼。
在上面的示例中,有一個嚴(yán)重的問題:如果在with塊中拋出了異常,Python解釋器會捕獲,然后在looking_glass函數(shù)的yeild表達式再次拋出。但是問題是沒有處理錯誤的代碼,那么looking_glass函數(shù)就會終止,永遠的無法恢復(fù)成sys.stdout.write方法原始個功能,導(dǎo)致系統(tǒng)的輸出處于無效狀態(tài)。
所以要添加一下異常的處理,比如ZeroDivisionError異常。
示例,添加異常處理的基于生成器的上下文管理器
import contextlib @contextlib.contextmanager def looking_glass(): """鏡子:看到的字是反的""" import sys original_write = sys.stdout.write # 把原始的[屏幕打印輸出]函數(shù)保存到一個實例屬性中,供以后使用 def reverse_write(text): original_write(text[::-1]) sys.stdout.write = reverse_write # 猴子補?。禾鎿Q成自己的方法實現(xiàn) msg = '' try: yield "ABCD" # 只需要捕獲yield部分 except ZeroDivisionError: msg = 'do not divide by zero' finally: sys.stdout.write = original_write if msg: print(msg) with looking_glass() as what: 0 / 0 # 拋出異常 print('lijiachang') print(what) print(what) print('back to normal')
打印
do not divide by zero
ABCD
back to normal
知識點:
- 在生成器函數(shù)中只需要捕獲yeild關(guān)鍵字這行的異常,Python解釋器會在with塊中的異常,轉(zhuǎn)移到y(tǒng)ield這行拋出。
- 所以在使用@contextmanager裝飾器時,要把yeild語句放到try/finally語句中,因為我們永遠不知道上下文管理器的用戶會在with塊中做什么。
關(guān)于異常的處理的對比:
- 在用類實現(xiàn)上下文管理器時,前面說過,為了告訴解釋器異常已經(jīng)處理過了,需要在__exit__方法中返回True,此時解釋器會壓制異常。如果__exit__沒有顯示的返回一個值,那么解釋器得到的就是None,此時會向上冒泡異常。
- 在用@contextmanager裝飾器時,默認的行為是相反的:裝飾器提供的__exit__方法假定發(fā)給生成器的所有異常都已經(jīng)處理了,因此默認壓制異常。如果不想讓contextmanager壓制異常,必須在裝飾的函數(shù)中顯式的重新拋出異常。
最后,再次強調(diào):在@contextmanager裝飾器裝飾去生成器中,yield與迭代沒有任何關(guān)系。
到此這篇關(guān)于Python上下文管理器詳細使用教程的文章就介紹到這了,更多相關(guān)Python上下文管理器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python構(gòu)造函數(shù)與析構(gòu)函數(shù)超詳細分析
在python之中定義一個類的時候會在類中創(chuàng)建一個名為__init__的函數(shù),這個函數(shù)就叫做構(gòu)造函數(shù)。它的作用就是在實例化類的時候去自動的定義一些屬性和方法的值,而析構(gòu)函數(shù)恰恰是一個和它相反的函數(shù),這篇文章主要介紹了Python構(gòu)造函數(shù)與析構(gòu)函數(shù)2022-11-11Windows下Pycharm遠程連接虛擬機中Centos下的Python環(huán)境(圖文教程詳解)
由于最近學(xué)習(xí)tensorflow的需要,tensorflow是在Linux環(huán)境下,使用的是Python。為了方便程序的調(diào)試,嘗試在Windows下的Pycharm遠程連接到虛擬機中Centos下的Python環(huán)境,感興趣的朋友跟隨小編看看吧2020-03-03詳細介紹pandas的DataFrame的append方法使用
這篇文章主要介紹了詳細介紹pandas的DataFrame的append方法使用,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07python?包實現(xiàn)JSON?輕量數(shù)據(jù)操作
這篇文章主要介紹了python?包實現(xiàn)JSON?輕量數(shù)據(jù)操作,文章介紹內(nèi)容首先將對象轉(zhuǎn)為json字符串展開主題詳細內(nèi)容需要的小伙伴可以參考一下2022-04-04Python 在OpenCV里實現(xiàn)仿射變換—坐標(biāo)變換效果
這篇文章主要介紹了Python 在OpenCV里實現(xiàn)仿射變換—坐標(biāo)變換效果,本文通過一個例子給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-08-08