Python上下文管理器詳細(xì)使用教程
with語句會設(shè)置一個臨時的上下文,交給上下文管理器對象控制,并且負(fù)責(zé)清理上下問題。
這樣做能避免錯誤并減少樣板代碼,因此API能更安全,更易使用。除了自動關(guān)閉文件之外,with塊還有很多用途。
上下文管理器和with塊
上下文管理器對象目的是管理with語句,就像迭代器的存在是為了管理for語句一樣。
with語句的目的是簡化try/finnally模式。
這種模式用于保證一段代碼運(yùn)行完畢后執(zhí)行某項(xiàng)操作,即便那段代碼是由于異常、return語句、sys.exit()調(diào)用而終止的,也都會執(zhí)行指定的finally操作。finally子句中通常存放用于釋放重要資源,或者還原臨時變更的狀態(tài)。
上下文管理器協(xié)議包含__enter__和__exit__兩個方法。
with語句開始運(yùn)行時,會在上下文管理對象上調(diào)用__enter__方法;with語句運(yùn)行結(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.
知識點(diǎn):
- fp變量在上下文管理器之外,依舊存在,可以讀取fp對象屬性。因?yàn)閣ith塊于函數(shù)和模塊不同,沒有定義新的作用域。
- 但是 不能在fp上再執(zhí)行IO操作,因?yàn)樵趙ith塊的末尾,已經(jīng)調(diào)用了TextIOWrapper__exit__方法把文件關(guān)閉了。
執(zhí)行with后面的表達(dá)式的結(jié)果是上下文管理器對象,不過,把值綁定到目標(biāo)變量(as后的變量)是在上下文管理器對象上調(diào)用__enter__方法的結(jié)果。
不管控制流程以哪種方式退出with塊,都會在上下文管理器對象上調(diào)用__exit__方法,而不是在__enter__方法返回的對象上調(diào)用。
with語句的as子句是可選的。對于像open這樣的函數(shù)來說,必須加上as子句,以便獲取文件的對象引用。不過一些上下文管理器對象會返回None,因?yàn)闆]有什么有用的對象給用戶提供。
示例,實(shí)現(xiàn)一個LookingGlass類,上下文管理器
class LookingGlass:
"""鏡子:看到的字是反的"""
def __enter__(self):
import sys
self.original_write = sys.stdout.write # 把原始的[屏幕打印輸出]函數(shù)保存到一個實(shí)例屬性中,供以后使用
sys.stdout.write = self.reverse_write # 猴子補(bǔ)?。禾鎿Q成自己的方法實(shí)現(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
知識點(diǎn):
- 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í)例。有時會有參數(shù)傳遞給異常構(gòu)造方法,例如錯誤信息,這些參數(shù)使用exc_value.args獲取
traceback: traceback對象。
補(bǔ)充,在try/finally語句的finally塊中調(diào)用sys.exc_info()得到的就是__exit__接收的這三個參數(shù)。
- 在__exit__中返回True是告訴解釋器,異常已經(jīng)處理了。如果返回True之外的值,比如默認(rèn)的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)。因?yàn)閟tdout的所有輸出都經(jīng)過了__enter__方法中打補(bǔ)丁的reverse_write方法實(shí)現(xiàn)。
contextlib模塊
Python標(biāo)準(zhǔn)庫文檔中的contextlib模塊,提供了自定義上下文管理器的一些函數(shù)
- closing :如果對象提供了close()方法,但沒有實(shí)現(xiàn)__enter__/__exit__方法,可以使用這個函數(shù)來構(gòu)建上下文管理器。
- suppress : 構(gòu)建臨時忽略指定異常的上下文。
- @contextmanager :這個裝飾器可以把簡單的生成器變?yōu)樯舷挛墓芾砥?,這樣就不需要創(chuàng)建類來實(shí)現(xiàn)管理器協(xié)議了。
- ContextDecorate :這是個基類,用于編寫類可以繼承他,用于定義基于類的上下文管理器。也可以用于裝飾器函數(shù),在受管理的上下文中運(yùn)行整個函數(shù)。
- ExitStack : 這個上下文管理器能進(jìn)入多個上下文管理器。with塊結(jié)束時,按照后進(jìn)先出的順序調(diào)用棧中各個上下文管理器的__exit__方法。如果事先不知道 with塊要進(jìn)入多少個上下文管理器,可以使用這個類。
使用最廣泛的還是@contextmanager裝飾器。要注意,這個裝飾器與迭代無關(guān),卻要使用yeild關(guān)鍵字。
@contextmanager 裝飾器
使用@contextmanager裝飾器呢個減少創(chuàng)建上下文管理器的代碼量,因?yàn)椴挥镁帉懸粋€完整的類,不用定義__enter__和__exit__方法,只需要一個實(shí)現(xiàn)yeild語句的生成器,生成想讓__enter__方法返回的值。
其中yeild語句的作用是把函數(shù)的定義體分為兩部分:
yeild語句前面的代碼在with塊開始時(即解釋器調(diào)用__enter__方法時)執(zhí)行。
yeild語句后面的代碼在with塊結(jié)束時(即調(diào)用__exit__方法時)執(zhí)行。
示例,使用生成器實(shí)現(xiàn)上下文管理器
import contextlib
@contextlib.contextmanager
def looking_glass():
import sys
original_write = sys.stdout.write # 把原始的[屏幕打印輸出]函數(shù)保存到一個實(shí)例屬性中,供以后使用
def reverse_write(text):
original_write(text[::-1])
sys.stdout.write = reverse_write # 猴子補(bǔ)?。禾鎿Q成自己的方法實(shí)現(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')
知識點(diǎn) :
- yield 后面的值會綁定到with語句中as子句的目標(biāo)變量上,執(zhí)行with塊中的代碼,這個函數(shù)會在這里暫停。
- 控制權(quán)一旦調(diào)成with塊,就會繼續(xù)執(zhí)行yeild語句后的代碼
@contextmanager 原理和注意事項(xiàng)
其實(shí),contextlib.contextmanager裝飾器會把函數(shù)包裝實(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表達(dá)式再次拋出。但是問題是沒有處理錯誤的代碼,那么looking_glass函數(shù)就會終止,永遠(yuǎn)的無法恢復(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ù)保存到一個實(shí)例屬性中,供以后使用
def reverse_write(text):
original_write(text[::-1])
sys.stdout.write = reverse_write # 猴子補(bǔ)?。禾鎿Q成自己的方法實(shí)現(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
知識點(diǎn):
- 在生成器函數(shù)中只需要捕獲yeild關(guān)鍵字這行的異常,Python解釋器會在with塊中的異常,轉(zhuǎn)移到y(tǒng)ield這行拋出。
- 所以在使用@contextmanager裝飾器時,要把yeild語句放到try/finally語句中,因?yàn)槲覀冇肋h(yuǎn)不知道上下文管理器的用戶會在with塊中做什么。
關(guān)于異常的處理的對比:
- 在用類實(shí)現(xiàn)上下文管理器時,前面說過,為了告訴解釋器異常已經(jīng)處理過了,需要在__exit__方法中返回True,此時解釋器會壓制異常。如果__exit__沒有顯示的返回一個值,那么解釋器得到的就是None,此時會向上冒泡異常。
- 在用@contextmanager裝飾器時,默認(rèn)的行為是相反的:裝飾器提供的__exit__方法假定發(fā)給生成器的所有異常都已經(jīng)處理了,因此默認(rèn)壓制異常。如果不想讓contextmanager壓制異常,必須在裝飾的函數(shù)中顯式的重新拋出異常。
最后,再次強(qiáng)調(diào):在@contextmanager裝飾器裝飾去生成器中,yield與迭代沒有任何關(guān)系。
到此這篇關(guān)于Python上下文管理器詳細(xì)使用教程的文章就介紹到這了,更多相關(guān)Python上下文管理器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
padas 生成excel 增加sheet表的實(shí)例
今天小編就為大家分享一篇padas 生成excel 增加sheet表的實(shí)例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-12-12
pandas的唯一值、值計(jì)數(shù)以及成員資格的示例
今天小編就為大家分享一篇pandas的唯一值、值計(jì)數(shù)以及成員資格的示例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-07-07
Python的Django框架中forms表單類的使用方法詳解
用戶表單是Web端的一項(xiàng)基本功能,大而全的Django框架中自然帶有現(xiàn)成的基礎(chǔ)form對象,Python的Django框架中forms表單類的使用方法詳解2016-06-06
pycharm社區(qū)版安裝django并創(chuàng)建一個簡單項(xiàng)目的全過程
社區(qū)版的pycharm跟專業(yè)版的pycharm應(yīng)用差別還是不太大,下面這篇文章主要給大家介紹了關(guān)于pycharm社區(qū)版安裝django并創(chuàng)建一個簡單項(xiàng)目的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-05-05
Python實(shí)現(xiàn)從log日志中提取ip的方法【正則提取】
這篇文章主要介紹了Python實(shí)現(xiàn)從log日志中提取ip的方法,涉及Python文件讀取、數(shù)據(jù)遍歷、正則匹配等相關(guān)操作技巧,需要的朋友可以參考下2018-03-03
Python源碼學(xué)習(xí)之PyType_Type和PyBaseObject_Type詳解
今天給大家?guī)淼氖顷P(guān)于Python源碼的相關(guān)知識學(xué)習(xí),文章圍繞著PyType_Type和PyBaseObject_Type展開,文中有非常詳細(xì)的介紹及代碼示例,需要的朋友可以參考下2021-06-06
Python Excel實(shí)現(xiàn)自動添加編號
這篇文章主要為大家詳細(xì)介紹了如何使用Python在Excel中實(shí)現(xiàn)自動添加編號效果,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-03-03

