深入學習Python中的上下文管理器與else塊
前言
本文主要個大家介紹了關于Python上下文管理器與else塊的相關內(nèi)容,分享出來供大家參考學習,下面話不多說了,來一起看看詳細的介紹吧。
在開始之前,我們先來看看下面這段話:
最終,上下文管理器可能幾乎與子程序(subroutine)本身一樣重要。目前,我們只了解了上下文管理器的皮毛……Basic 語言有with 語句,而且很多語言都有。但是,在各種語言中 with 語句的作用不同,而且做的都是簡單的事,雖然可以避免不斷使用點號查找屬性,但是不會做事前準備和事后清理。不要覺得名字一樣,就意味著作用也一樣。with 語句是非常了不起的特性。
——Raymond Hettinger
雄辯的 Python 布道者
先做這個,再做那個:if語句之外的else塊
這個語言特性不是什么秘密,但卻沒有得到重視:else 子句不僅能在if 語句中使用,還能在 for、while 和 try 語句中使用。for/else、while/else 和 try/else 的語義關系緊密,不過與if/else 差別很大。起初,else 這個單詞的意思阻礙了我對這些特性的理解,但是最終我習慣了。
else 子句的行為如下:
for
僅當 for 循環(huán)運行完畢時(即 for 循環(huán)沒有被 break 語句中止)才運行 else 塊。
while
僅當 while 循環(huán)因為條件為假值而退出時(即 while 循環(huán)沒有被break 語句中止)才運行 else 塊。
try
僅當 try 塊中沒有異常拋出時才運行 else 塊。官方文檔(https://docs.python.org/3/reference/compound_stmts.html)還指出:“else 子句拋出的異常不會由前面的 except 子句處理?!?/p>
注意:
在所有情況下,如果異?;蛘?return、break 或 continue 語句導致控制權跳到了復合語句的主塊之外,else 子句也會被跳過。
在這些語句中使用 else 子句通常能讓代碼更易于閱讀,而且能省去一些麻煩,不用設置控制標志或者添加額外的 if 語句。
在循環(huán)中使用 else 子句的方式如下述代碼片段所示:
for item in my_list: if item.flavor == 'banana': break else: raise ValueError('No banana flavor found!')
一開始,你可能覺得沒必要在 try/except 塊中使用 else 子句。畢竟,在下述代碼片段中,只有 dangerous_call()
不拋出異常,after_call()
才會執(zhí)行,對吧?
try: dangerous_call() after_call() except OSError: log('OSError...')
然而,after_call()
不應該放在 try 塊中。為了清晰和準確,try 塊中應該只拋出預期異常的語句。因此,像下面這樣寫更好:
try: dangerous_call() except OSError: log('OSError...') else: after_call()
現(xiàn)在很明確,try 塊防守的是 dangerous_call()
可能出現(xiàn)的錯誤,而不是 after_call()
。而且很明顯,只有 try 塊不拋出異常,才會執(zhí)行after_call()
。
上下文管理器和with塊
上下文管理器對象存在的目的是管理 with 語句,就像迭代器的存在是為了管理 for 語句一樣。
with 語句的目的是簡化 try/finally 模式。這種模式用于保證一段代碼運行完畢后執(zhí)行某項操作,即便那段代碼由于異常、return 語句或sys.exit()
調(diào)用而中止,也會執(zhí)行指定的操作。finally 子句中的代碼通常用于釋放重要的資源,或者還原臨時變更的狀態(tài)。
上下文管理器協(xié)議包含 __enter__ 和 __exit__ 兩個方法。with 語句開始運行時,會在上下文管理器對象上調(diào)用 __enter__ 方法。with 語句運行結束后,會在上下文管理器對象上調(diào)用 __exit__ 方法,以此扮演 finally 子句的角色。
🌰 演示把文件對象當成上下文管理器使用
>>> with open('mirror.py') as fp: # fp綁定到打開的文件上,因為文件的__enter__方法返回self ... src = fp.read(60) # 從fp中讀取一些數(shù)據(jù) ... >>> len(src) >>> fp # fp變量依然可以使用 <_io.TextIOWrapper name='mirror.py' mode='r' encoding='UTF-8'> >>> fp.closed, fp.encoding # 可以讀取fp對象的屬性 (True, 'UTF-8') >>> fp.read(60) # 但是不能在fp上執(zhí)行I/O操作,因為在with塊的結尾,調(diào)用了TextIOWrappper.__exit__方法把文件關閉了 Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: I/O operation on closed file.
測試 LookingGlass 上下文管理器類
>>> from mirror import LookingGlass >>> with LookingGlass() as what: # 上下文管理器是LookingGlass類的實例;Python在上下文管理器上調(diào)用__enter__方法,把返回結果綁定在what上 ... print('Alice, Kitty and Snowdrop') # 打印一個字符串,然后打印what變量的值 ... print(what) ... pordwonS dna yttiK ,ecilA # 打印出的內(nèi)容是反向的 YKCOWREBBAJ >>> what # 現(xiàn)在,with塊已經(jīng)執(zhí)行完畢,可以看出,__enter__方法返回的值,即存儲在what變量中的值是字符串'JABBERWOCKY' 'JABBERWOCKY' >>> print('Back to normal.') # 輸出不在是反向的了 Back to normal.
mirror.py:LookingGlass 上下文管理器類的代碼
class LookingGlass: def __enter__(self): # 除了 self 之外,Python調(diào)用__enter__方法時不竄入其他參數(shù) import sys self.original_write = sys.stdout.write # 把原來的 sys.stdout.write 方法保存在一個實例屬性中,供后面使用 sys.stdout.write = self.reverse_write # 為 sys.stdout.write 打猴子補丁,替換成自己編寫的方法 return 'JABBERWOCKY' # 返回 'JABBERWOCKY' 字符串,這樣才有內(nèi)容存入目標變量 what def reverse_write(self, text): # 這是用于取代 sys.stdout.write 的方法,把 text 參數(shù)的內(nèi)容反轉,然后調(diào)用原來的方法實現(xiàn) return self.original_write(text[::-1]) def __exit__(self, exc_type, exc_val, traceback): # 如果一切正常,Python會調(diào)用__exit__方法傳入的參數(shù)是三個None,如果拋出異常,則三個參數(shù)是異常的數(shù)據(jù) import sys sys.stdout.write = self.original_write # 還原成原來的sys.studout.write方法 if exc_type is ZeroDivisionError: # 如果有異常,而且是 ZeroDivisionError 類型,打印一個消息 print('Please DO NOT divide by zero!') return True # 然后返回 True,告訴解釋器,異常已經(jīng)處理
解釋器調(diào)用 __enter__ 方法時,除了隱式的 self 之外,不會傳入任何參數(shù)。傳給 __exit__ 方法的三個參數(shù)列舉如下。
exc_type
異常類(例如 ZeroDivisionError)
exc_value
異常實例。有時會有參數(shù)傳給異常構造方法,例如錯誤消息,這些參數(shù)可以使用 exc_value.args 獲取
traceback
traceback 對象
在 with 塊之外使用 LookingGlass 類
>>> from mirror import LookingGlass >>> manager = LookingGlass() # 實例化并審查manager實例,等同于 with LookingGlass() as manager >>> manager <mirror.LookingGlass object at 0x2a578ac> >>> monster = manager.__enter__() # 在上下文管理器中調(diào)用__enter__()方法,把結果存儲在monster中 >>> monster == 'JABBERWOCKY' # monster的值是字符串'JABBERWOCKY',打印出來的True標識符是反向,因為用了猴子補丁 eurT >>> monster 'YKCOWREBBAJ' >>> manager >ca875a2x0 ta tcejbo ssalGgnikooL.rorrim< >>> manager.__exit__(None, None, None) # 調(diào)用manager.__exit__,還原成之前的stdout.write >>> monster 'JABBERWOCKY'
contextlib模塊中的實用工具
closing
如果對象提供了 close()
方法,但沒有實現(xiàn)__enter__/__exit__ 協(xié)議,那么可以使用這個函數(shù)構建上下文管理器。
suppress
構建臨時忽略指定異常的上下文管理器。
@contextmanager
這個裝飾器把簡單的生成器函數(shù)變成上下文管理器,這樣就不用創(chuàng)建類去實現(xiàn)管理器協(xié)議了。
ContextDecorator
這是個基類,用于定義基于類的上下文管理器。這種上下文管理器也能用于裝飾函數(shù),在受管理的上下文中運行整個函數(shù)。
ExitStack
這個上下文管理器能進入多個上下文管理器。with 塊結束時,ExitStack 按照后進先出的順序調(diào)用棧中各個上下文管理器的__exit__ 方法。如果事先不知道 with 塊要進入多少個上下文管理器,可以使用這個類。例如,同時打開任意一個文件列表中的所有文件。
使用@contextmanager
@contextmanager 裝飾器能減少創(chuàng)建上下文管理器的樣板代碼量,因為不用編寫一個完整的類,定義 __enter__ 和 __exit__ 方法,而只需實現(xiàn)有一個 yield 語句的生成器,生成想讓 __enter__ 方法返回的值。
在使用 @contextmanager 裝飾的生成器中,yield 語句的作用是把函數(shù)的定義體分成兩部分:yield 語句前面的所有代碼在 with 塊開始時(即解釋器調(diào)用 __enter__ 方法時)執(zhí)行, yield 語句后面的代碼在with 塊結束時(即調(diào)用 __exit__ 方法時)執(zhí)行。
mirror_gen.py:使用生成器實現(xiàn)的上下文管理器
import contextlib @contextlib.contextmanager # 應用 contextmanager 裝飾器 def looking_glass(): import sys original_write = sys.stdout.write # 貯存原來的 sys.stdout.write 方法 def reverse_write(text): # 定義自定義的 reverse_write 函數(shù);在閉包中可以訪問 original_write original_write(text[::-1]) sys.stdout.write = reverse_write # 把 sys.stdout.write 替換成 reverse_write yield 'JABBERWOCKY' # 產(chǎn)出一個值,這個值會綁定到 with 語句中 as 子句的目標變量上 sys.stdout.write = original_write # 控制權一旦跳出 with 塊,繼續(xù)執(zhí)行 yield 語句之后的代碼;這里是恢復成原來的 sys. stdout.write 方法 with looking_glass() as what: # 直接通過上下文管理器實現(xiàn)with的功能 print('Alice, Kitty and Snowdrop') print(what) print(what)
以上代碼執(zhí)行的結果為:
pordwonS dna yttiK ,ecilA YKCOWREBBAJ JABBERWOCKY
其實,contextlib.contextmanager 裝飾器會把函數(shù)包裝成實現(xiàn)__enter__ 和 __exit__ 方法的類
這個類的 __enter__ 方法有如下作用:
(1) 調(diào)用生成器函數(shù),保存生成器對象(這里把它稱為 gen)。
(2) 調(diào)用 next(gen),執(zhí)行到 yield 關鍵字所在的位置。
(3) 返回 next(gen) 產(chǎn)出的值,以便把產(chǎn)出的值綁定到 with/as 語句中的目標變量上。
with 塊終止時,__exit__ 方法會做以下幾件事:
(1) 檢查有沒有把異常傳給 exc_type;如果有,調(diào)用gen.throw(exception)
, 在生成器函數(shù)定義體中包含 yield 關鍵字的那一行拋出異常。
(2) 否則,調(diào)用 next(gen)
,繼續(xù)執(zhí)行生成器函數(shù)定義體中 yield 語句之后的代碼。
注意:
上面的 🌰 有一個嚴重的錯誤:如果在 with 塊中拋出了異常,Python 解釋器會將其捕獲,然后在 looking_glass 函數(shù)的 yield 表達式里再次拋出。但是,那里沒有處理錯誤的代碼,因此 looking_glass 函數(shù)會中止,永遠無法恢復成原來的 sys.stdout.write
方法,導致系統(tǒng)處于無效狀態(tài)。
mirror_gen_exc.py:基于生成器的上下文管理器,而且實現(xiàn)了異常處理
import contextlib @contextlib.contextmanager def looking_glass(): import sys original_write = sys.stdout.write def reverse_write(text): original_write(text[::-1]) sys.stdout.write = reverse_write msg = '' #創(chuàng)建一個變量,用于保存可能出現(xiàn)的錯誤消息; try: yield 'JABBERWOCKY' except ZeroDivisionError: #處理 ZeroDivisionError 異常,設置一個錯誤消息 msg = 'Please DO NOT divide by zero!' finally: sys.stdout.write = original_write # 撤銷對 sys.stdout.write 方法所做的猴子補丁 if msg: print(msg) # 如果設置了錯誤消息,把它打印出來
注意:
使用 @contextmanager 裝飾器時,要把 yield 語句放在try/finally 語句中(或者放在 with 語句中),這是無法避免的,因為我們永遠不知道上下文管理器的用戶會在 with 塊中做什么。
總結
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學習或者工作能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關文章
Python數(shù)據(jù)分析之?Matplotlib?散點圖繪制
這篇文章主要介紹了Python數(shù)據(jù)分析之?Matplotlib?散點圖繪制,散點圖又稱散點圖,是使用多個坐標點的分布反映數(shù)據(jù)點分布規(guī)律、數(shù)據(jù)關聯(lián)關系的圖表,下文對散點圖的詳細介紹及繪制,需要的小伙伴可以參考以一下2022-05-05python?sklearn與pandas實現(xiàn)缺失值數(shù)據(jù)預處理流程詳解
對于缺失值的處理,主要配合使用sklearn.impute中的SimpleImputer類、pandas、numpy。其中由于pandas對于數(shù)據(jù)探索、分析和探查的支持較為良好,因此圍繞pandas的缺失值處理較為常用2022-09-09