一切皆對象——Python面向對象:上下文管理器(下)
上下文管理器中的異常處理
這篇文章我們來看一下上下文管理器中的異常處理和標準庫對於上下文管理器的支持。
回顧一下上下文管理器的特點:上下文管理器是個對象,它有__enter__
和__exit__
兩個方法。
class Context: def __enter__(self): print(In enter) return self def __exit__(self, *_): print(In exit) return True
這裡的__exit__
方法的參數列表被我們利用*_
收集到了一起,我們把它列印出來看看是什麼內容:
class Context: def __enter__(self): return self def __exit__(self, *_): from pprint import pprint pprint(_) return Truewith Context(): print(In context)# In context# (None, None, None)
所以說,在離開上下文時,解釋器會給__exit__
額外傳遞3個位置參數。這些參數都是用於處理上下文中的異常的,所以正常狀態下,他們都是None
。讓我們嘗試在上下文中拋出一個異常:
with Context(): raise Exception(Raised) # (<class Exception>,# Exception(Raised,),# <traceback object at 0x0000025A35441D88>)
我們依照一個普通的處理異常的語句來看一下這三個參數都是什麼:
try: raise Exception(Raised)except Exception as e: print(type(e)) print(repr(e)) print(e.__traceback__) # (<class Exception>,# Exception(Raised,),# <traceback object at 0x0000025A35441D88>)
可以看到,__exit__
的三個參數分別表示:
- 異常類型;
- 異常對象(關於
repr
將在字元串系列中詳細說明); - 棧對象;
那麼,為什麼在上下文中拋出了異常,程序卻沒有異常中止呢?答案在於__exit__
的返回值。如果它返回了True
,那麼上下文中的異常將被忽略;如果是False
,那麼上下文中的異常將被重新向外層拋出。假如在外層沒有異常處理的代碼,那麼程序將會崩潰:
class Context: def __enter__(self): return self def __exit__(self, *_): # 返回一個False return Falsewith Context(): raise Exception(Raised) # Traceback (most recent call last):# File "C:...py", line 33, in <module># raise Exception(Raised)# Exception: Raised
那麼,如何在__exit__
中處理異常呢?既然能夠獲取到異常對象,那麼可以通過isinstance
來判斷異常類型,或是直接利用參數中的異常類型來判斷,進而做出相應處理:
exs = [ ValueError, IndexError, ZeroDivisionError,]class Context: def __enter__(self): return self def __exit__( self, ex_type, ex_value, tb ): if ex_type in exs: print(handled) return True else: return Falsewith Context(): 10 / 0# handled try: with Context(): raise TypeError()except TypeError: print(handled outside)# handled outside
那麼,如果在__enter__
里可能出現異常,我們該怎麼辦呢?很不幸,我們只能在__enter__
里去手動try...except...
它們。
標準庫的支持
Python標準庫contextlib
中給出了上下文管理器的另一種實現:contextmanager
。它是一個裝飾器。我們來簡單看一下它是怎麼使用的:
from contextlib import contextmanager@contextmanagerdef context(): print(In enter) yield print(In exit) with context(): print(In context) # In enter# In context# In exit
來和我們最初的寫法比較一下:
class Context: def __enter__(self): print(In enter) return self def __exit__(self, *_): print(In exit) return Truewith Context(): print(In context) # In enter# In context# In exit
結果一樣,但寫法簡單了許多。關於yield
關鍵字,後面我們會詳細介紹。這裡我們只需要知道,在yield
之前的語句扮演了__enter__
的角色,而在yield
之後的語句則扮演了__exit__
的角色。那麼,我們如何像__enter__
一樣返回一個對象呢?例如,我們打開一個文件:
@contextmanagerdef fileopen(name, mod): f = open(name, mod) # 直接yield出去即可 yield f f.close() with fileopen(a.txt, r) as f: for line in f: print(line)
如何處理這裡面的異常呢?在yield
處採用try...except...finally
語句:
@contextmanagerdef fileopen(name, mod): try: f = open(name, mod) yield f except: print(handled) finally: f.close() with fileopen(a.txt, r) as f: raise Exception()# handled
實際上,對於這類需要在離開上下文後調用close
方法釋放資源的對象,contextlib
給出了更加直接的方式:
from contextlib import closingclass A: def close(self): print(Closing)with closing(A()) as a: print(a)# <__main__.A object at 0x00000264464E50B8># Closing
這樣,類A
的對象自動變成了上下文管理器對象,並且在離開這個上下文的時候,解釋器會自動調用對象a
的close
方法(即使中間拋出了異常)。所以,針對一些具有close
方法的非上下文管理器對象,直接利用closing
要便捷許多。
contextlib
還提供了另外一種不使用with
的語法糖來實現上下文功能。採用這種方式定義的上下文只是增加了一個繼承關係:
from contextlib import ContextDecoratorclass Context(ContextDecorator): def __enter__(self): print(In enter) return self def __exit__(self, *_): print(In exit) return True
怎麼使用呢?請看:
@Context()def context_func(): print(In context)context_func()# In enter# In context# In exit
上下文代碼不再使用with
代碼段,而是定義成函數,通過裝飾器的方式增加了一個進入和離開的流程。我們可以根據實際情況,靈活地採取不同的寫法來實現我們的功能。
最後,我們再來看一個contextlib
提供的功能:suppress
。它可以創建一個能夠忽略特定異常的上下文管理器。有些時候,我們可能知道上下文管理器中的代碼可能拋出什麼異常,或者說我們不關心拋出了哪些異常,我們可以讓__exit__
函數直接返回True
,這樣所有的異常就被忽略在了__exit__
中。suppress
提供了一個更簡便的寫法,我們只需給它傳入需要忽略的異常類型即可:
from contextlib import suppressig_exs = [ ValueError, IndexError, RuntimeError, OSError, ...,]with suppress(*ig_exs): raise ValueError()print(Nothing happens)# Nothing happens
因為所有的非系統異常都是Exception
的子類,所以如果參數傳入了Exception
,那麼所有的異常都會被忽略:
from contextlib import suppresswith suppress(Exception): raise OverflowError()print(Nothing happens)# Nothing happens
這裡需要說明的是何為非系統異常。有一些異常可能來自系統問題而非程序本身,例如我們經常有經驗,程序陷入死循環了,我們需要用Ctrl-c
結束它。如果你注意了Ctrl-c
後程序列印的錯誤信息,會發現它拋出了一個KeyboardInterrupt
。類似這些異常(包括Exception
本身)都繼承於BaseException
。所以,真正的異常的父類是BaseException
。關於異常的層次關係,請參閱:https://docs.python.org/3/library/exceptions.html#exception-hierarchy
推薦閱讀:
※pip 安裝第三方庫出現問題????求助
※Python爬取起點中文網小說排行榜信息(上海線下培訓作業)
※對神秘巨星影評製作詞雲
※第十二期 · 「正統」前端開發(上):了解NPM與Vue開發,並結合PyCharm搭建前端開發環境
※使用Python數據透視表挖掘幸福數據
TAG:Python |