目錄
2.3 try-except-else-finally 語句
四、異常自定義 (exception customizing)
八、彩蛋 —— finally 和 return 誰的優先級更高?(選讀)
一、緒論 (introduction)
不同於語法錯法錯誤 (解析錯誤),調試 Python 程序時,即便語句或表達式的語法正確,也可能在執行時引發錯誤。在 執行時檢測到的錯誤 稱爲 異常。Python 使用被稱爲 異常 的 特殊對象 來管理程序執行期間發生的錯誤。每當發生讓 Python 不知所措的錯誤時,它都會創建一個異常對象。
異常雖不一定會導致嚴重後果,但大多數異常並不會被程序處理。當 Python 腳本發生異常時,程序將終止執行,並顯示各種 回溯 (Traceback) 信息。Traceback 是 Python 錯誤信息的報告,類似於其他編程語言中的 stack trace、stack traceback、backtrac 等。Traceback 的前一部分以堆棧回溯的形式顯示發生異常時的上下文,並由語法分析器指示出錯的行,而最後一行則聲明程序的錯誤類型信息。
尤其是在讀寫文件時,很多地方都可能導致錯誤發生。例如,試圖讀取一個不存在的文件或目錄時,將得到一個找不到文件的錯誤 (FileNotFoundError):
>>> fin = open('test.py')
Traceback (most recent call last):
File "<pyshell#1>", line 1, in <module>
fin = open('test.py')
FileNotFoundError: [Errno 2] No such file or directory: 'test.py'
關於異常的原因,一方面,可能源自個人疏忽與考慮不周,此時需要根據異常 Traceback 到出錯位置並分析改正;另一方面,有些異常 無法預料或不可避免,此時可選擇 捕獲異常並處理,從而 避免程序的意外終止或崩潰。
二、異常捕獲 (exception catching)
使用 try 語句 代碼塊是最基本的 異常捕獲和處理 方法,其常見的搭配形式 (關鍵詞組合) 有:
- try-except
- try-except-else
- try-except-else-finally
當然,對於關鍵詞的選用與搭配,實質還是取決於個人需求。以下將依次說明:
2.1 try-except 語句
2.1.1 基本用法
try-except 語句是 最基礎而重要 的部分,其基本語法規則爲:
try:
# 執行要嘗試 (try) 的代碼
except:
# 執行應對異常發生時的代碼
try-except 語句用於檢測 try 子句(塊) 中的錯誤,從而令 except 語句(塊) 捕獲異常信息並作出應對和處理。具體而言,Python 從 try 子句開始執行,若一切正常,則跳過 except 子句;若發生異常,則跳出 try 子句,執行 except 子句。
延續上節例子 —— 捕獲讀取一個不存在的文件/目錄的異常:
>>> try:
fin = open('test.py') # 不存在的文件
print('Everything went well!') # 打印順利運行提示信息
except:
print('Something went wrong!') # 處理異常方式:打印錯誤提示信息
Something went wrong!
可見異常被捕獲了,IDLE 並未打印 Traceback 信息,而是打印了我們自定義的 except 子句中的錯誤提示信息。然而,本例中的 except 子句僅僅是簡單地提示了錯誤。實際上,可以根據需求設計更多具有實用修正/彌補功能的 except 子句。
另一方面,若文件/目錄存在,則將順利執行完 try 子句並跳過 except 子句:
>>> try:
fin = open('train.py') # 實際存在的文件
print('Everything went well!') # 打印順利運行提示信息
except:
print('Something went wrong!') # 處理異常方式:打印錯誤提示信息
Everything went well!
2.1.2 指定異常類型
因爲 except 子句默認捕獲的異常類型是 Exception,所以 except 子句總是捕獲所有異常。
>>> try:
fin = open('test.py')
print('Everything went well!')
except Exception: # 不指定 Exception 也一樣
print('Something went wrong!')
Something went wrong!
但若有特殊需要,也可 指定 except 子句捕獲的異常類型,例如:
>>> try:
fin = open('test.py')
print('Everything went well!')
except FileNotFoundError:
print('Something went wrong!')
Something went wrong!
關於異常類型指定,既可以後知後覺 —— 根據 Trackback 指出的錯誤,也可以先知先覺 —— 查文檔選定以防不測。但注意,倘若發生了未指定到的異常類型 (通常源於誤指定或漏指定導致異常類型不匹配),則異常仍會發生:
>>> try:
fin = open('test.py')
print('Everything went well!')
except KeyError: # 雖然可以 catch KeyError, 但發生了 FileNotFoundError
print('Something went wrong!')
Traceback (most recent call last):
File "<pyshell#19>", line 2, in <module>
fin = open('test.py')
FileNotFoundError: [Errno 2] No such file or directory: 'test.py'
因此,若僅需要捕獲異常,不指定 except 語句捕獲的異常類型將更爲保險和省事 (畢竟異常類型辣麼多...)。
與此同時,若要 捕獲處理指定類型異常,一方面,可以 將需要捕獲的異常類型全都放在同一個 tuple 中:
>>> try:
fin = open('eval.py')
print('Everything goes well!')
except (FileExistsError, FileNotFoundError): # 異常類型 tuple
print('There is a FileExistsError or FileNotFoundError!')
There is a FileExistsError or FileNotFoundError!
這樣做的優點是簡潔明瞭,統一捕獲異常處理;缺點是不能夠“特事特辦” —— except 子句的異常處理將缺乏針對性。
爲實現對多種不同的特定類型異常的 分別捕獲處理,可以 令一個 try 語句對應多個 except 語句,例如:
>>> try:
fin = open('eval.py')
print('Everything goes well!')
except FileExistsError: # 捕獲特定類型異常
print('There is a FileExistsError!')
except FileNotFoundError: # 捕獲特定類型異常
print('There is a FileNotFoundError!')
There is a FileNotFoundError!
多個 except 子句串行執行,對於異常 “有則捕獲,無則通過”。注意,若發生的異常和 except 子句指定的異常類是同一個類或者是其基類,則可以 兼容並照常捕獲處理 (比如異常指定爲 Exception 時可捕獲大部分的異常,因爲所有內置的非系統退出類異常/用戶自定義異常都派生自此類),但 反之不成立 (except 子句指定的異常是實際發生異常的子類/派生類時則無法捕獲)。
此外,還可以使用 as 關鍵字指定 except 語句所捕獲異常的別名,
>>> try:
fin = open('eval.py')
print('Everything goes well!')
except FileNotFoundError as error: # as 關鍵字指定異常別名
print("Error information:{0}".format(error))
Error information:[Errno 2] No such file or directory: 'eval.py'
2.1.3 小結
總而言之,常見的用法仍是:令前面的 except 子句指定特定類型異常,令最後一個 except 子句忽略異常名以用作通配符,然後打印一個未知錯誤信息,並用 raise 關鍵字拋出異常。如下所示:
>>> import sys
>>> try:
fin = open('eval.py')
print('Everything goes well!')
except FileExistsError as error:
print("Error information:{0}".format(error))
except:
print("Unexpected error:", sys.exc_info()[0])
raise # 拋出異常
Unexpected error: <class 'FileNotFoundError'>
Traceback (most recent call last):
File "<pyshell#36>", line 2, in <module>
fin = open('eval.py')
FileNotFoundError: [Errno 2] No such file or directory: 'eval.py'
以上即爲篇幅最大、最爲基本而詳實的用法說明。
2.2 try-except-else 語句
try 語句除了可以後跟一至多個 except 子句,還可以選用 else 子句。若使用 else 子句,則必須將其後接於 except 子句後,且只能有一個 else 子句。else 子句將在 try 子句未發生任何異常時執行。
>>> try:
fin = open('oneline.txt')
print('Everything goes well!')
except FileExistsError:
print('There is a FileExistsError!')
except FileNotFoundError:
print('There is a FileNotFoundError!')
else:
print(fin.readlines()) # 讀取一行
fin.close() # 關閉/釋放文件對象 fin
Everything goes well!
['I Love Python!']
上例順利執行 try 語句,讀取了一個只有一行的 txt 文件,打印出成功讀取信息,並因此跳過各個 except 子句。然後,執行 else 子句,讀取 txt 文件的一行內容並打印之,最後關閉 fin 文件對象。
通常,使用 else 子句比將所有語句都放在 try 語句中靈活性更強,效果更好,因爲如此 可避免一些難以預料且 except 無法捕獲的異常。異常處理並不僅僅處理那些直接發生在 try 語句中的異常,而且還 能處理子句中調用的函數 (甚至間接調用的函數) 裏拋出的異常。例如:
>>> def wrong():
num = 6 / 0
>>> try:
wrong()
except ZeroDivisionError as error:
print('Handling run-time error:', error)
Handling run-time error: division by zero
總之,對於在 try子句不引發異常時必須執行的代碼而言,else 子句很有用。
2.3 try-except-else-finally 語句
除了 else 子句,還有另一個常用可選子句 —— finally 子句。若使用 finally 子句,則必須將其後 接於最後,且 只能有一個 finally 子句。無論異常有無發生,finally 子句都將執行。因此,finally 子句常用於存放一些必定要執行的內容或操作,例如:
>>> try:
fin = open('oneline.txt')
print('Everything goes well!')
except FileExistsError:
print('There is a FileExistsError!')
except FileNotFoundError:
print('There is a FileNotFoundError!')
else:
print(fin.readlines())
fin.close()
finally:
print("Operations are Finished!")
Everything goes well!
['I Love Python!']
Operations are Finished!
finally 子句常用於定義 無論在任何情況下都會執行的清理行爲。若一個異常在 try 子句裏 (或在 except 子句和 else 子句裏) 被拋出,而又沒有任何的 except 子句將其捕獲,那麼該異常 將會在 finally 子句執行後被拋出。例如:
>>> def divide(x, y):
try:
result = x / y
except ZeroDivisionError:
print("division by zero!")
else:
print("the result is", result)
finally:
print("executing finally clause")
>>> divide('6', '3')
executing finally clause
Traceback (most recent call last):
File "<pyshell#18>", line 1, in <module>
divide('6', '3')
File "<pyshell#17>", line 3, in divide
result = x / y
TypeError: unsupported operand type(s) for /: 'str' and 'str'
2.4 小結
try-except-else-finally 語句簡圖:
三、異常拋出 (exception raising)
Python 通過 raise 語句強制拋出一個指定異常,其語法格式爲:
raise [Exception [, args [, traceback]]]
raise 的唯一參數即 要拋出的指定異常,該參數必須是一個異常實例或異常類 (即派生自 Exception 的類)。若傳遞的是一個異常類,它將通過調用無參數的構造函數實現隱式實例化。
若只想確定是否拋出了異常而並不想去處理它,那麼一個 無參數的 raise 語句 便可 將當前在處理的異常再次拋出。例如:
>>> try:
raise NameError('HiThere') # 指定拋出異常名及其 Trackback 提示語
except NameError:
print('An exception flew by!')
An exception flew by!
# ------------------------------------------------------------------------------
>>> try:
raise NameError('Hello') # 指定拋出異常名及其 Trackback 提示語
except NameError:
print('An exception flew by!')
raise # 再次拋出, 對比上例
An exception flew by!
Traceback (most recent call last):
File "<pyshell#1>", line 2, in <module>
raise NameError('Hello')
NameError: Hello
如果令最後一個 raise 語句指定另一個類型的異常,則 Traceback 將按發生順序顯示這些 串聯 的異常信息:
>>> try:
raise NameError('Hello')
except NameError:
print('An exception flew by!')
raise KeyError
An exception flew by!
Traceback (most recent call last):
File "<pyshell#8>", line 2, in <module>
raise NameError('Hello')
NameError: Hello
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<pyshell#8>", line 5, in <module>
raise KeyError
KeyError
此外,隱式的異常上下文還可通過使用 raise ... from ... 語句來補充顯式的原因,限於篇幅不作詳述。
總之,使用 raise 語句可引發內置異常,通常用於測試異常處理程序或報告錯誤條件。
四、異常自定義 (exception customizing)
在 Python 中,所有異常必須爲一個派生自 BaseException 的類的實例 (BaseException 是所有內置異常的基類)。在帶有指定一個特定類的 except 子句的 try 語句中,該子句將處理派生自 BaseException 類的異常類 (但也有例外)。通過子類化創建的兩個不相關異常類永遠不等效的,即便二者名稱相同。
除了 Python 內置異常類,還可以將內置異常類子類化以定義新的異常。因爲 BaseException 類不應被用戶自定義類直接繼承,所以 鼓勵從 Exception 類或其子類來派生新的異常。
例如,可以直接或間接繼承 Exception 類實現一個自定義的異常類:
>>> class MyError(Exception):
''' 自定義異常類需要繼承自 Exception 類 '''
def __init__(self, value): # 重寫父類 Exception 的構造方法 __init__() 以覆蓋之
self.value = value
def __str__(self):
return repr(self.value)
>>> try:
raise MyError(6)
except MyError as error:
print('My exception occurred, value:', error.value)
My exception occurred, value: 6
自定義異常類可執行任何其他類能執行的任何操作,但實現時通常只提供許多屬性和少量方法,以允許處理程序爲異常提取有關錯誤的信息的同時確保簡潔性。
此外,在創建可能引發多個不同異常的模塊時,通常的做法是爲該模塊定義的各種異常創建一個基類 (作爲基礎的異常類),然後基於該基類爲不同的錯誤條件創建不同的子類。例如:
class Error(Exception):
"""Base class for exceptions in this module."""
pass
class InputError(Error):
"""Exception raised for errors in the input.
Attributes:
expression -- input expression in which the error occurred
message -- explanation of the error
"""
def __init__(self, expression, message):
self.expression = expression
self.message = message
class TransitionError(Error):
"""Raised when an operation attempts a state transition that's not
allowed.
Attributes:
previous -- state at beginning of transition
next -- attempted new state
message -- explanation of why the specific transition is not allowed
"""
def __init__(self, previous, next, message):
self.previous = previous
self.next = next
self.message = message
大多數異常的名稱都定義爲以 Error 結尾,類似於標準異常的命名。
五、預定義的清理行爲 (with statement)
在 Python 中,一些對象定義了在不再需要該對象時要執行的標準清理行爲,無論使用該對象的操作成敗與否。例如,下面的示例它嘗試打開一個文件並打印其內容:
for line in open("test.txt"):
print(line, end="")
這段代碼的問題在於,當執行完畢後,文件會在一段不確定的時間內保持打開狀態,而未被顯式地關閉!這在簡單腳本中無所謂,但對較大的應用程序而言可能是個問題。
因爲 with 語句可以實現資源的精確分配與釋放,所有在本例場景下,with 語句能夠保證諸如文件之類的對象在使用完後,一定會正確地執行其清理方法,例如:
>>> with open("train.txt") as f:
for line in f:
print(line, end="")
從而,執行完語句後,既便上述代碼在處理過程中出現問題,也能夠確保文件 f 總是被關閉。
關於 with 語句 實現的 上下文管理器 (Context manager) ,詳見《【Python】詳解 with 語句 (上下文管理器) —— 異常處理與完全解讀(下) 》。
六、斷言 (asserting)
除了上述引發異常的方式,Python 中還有一個 assert 斷言語句能夠觸發異常。 assert 語句常用於 判斷表達式,並 在表達式條件爲 False 時觸發異常 (準確地說是表達式的 bool 邏輯值爲 False 時)。其語法格式爲:
assert expression
實質等價於:
if not expression:
raise AssertionError
例如:
>>> assert True
>>> assert False # 表達式的 bool 邏輯值爲 False 將引發異常
Traceback (most recent call last):
File "<pyshell#22>", line 1, in <module>
assert False
AssertionError
>>> assert 1 > 0
>>> assert 1 < 0 # 表達式的 bool 邏輯值爲 False 將引發異常
Traceback (most recent call last):
File "<pyshell#25>", line 1, in <module>
assert 1 < 0
AssertionError
與此同時,assert 後也可指定參數:
assert expression [, arguments]
實質等價於:
if not expression:
raise AssertionError(arguments)
例如:
>>> assert 1 == 0, '1 is not equal to 0'
Traceback (most recent call last):
File "<pyshell#27>", line 1, in <module>
assert 1 == 0, '1 is not equal to 0'
AssertionError: 1 is not equal to 0
總之,assert 語句能夠在條件不滿足程序運行的情況下直接返回錯誤,而不必等待程序運行後出現崩潰的情況。例如,某代碼只能在 Linux 下運行,於是可以先判斷當前系統是否符合條件:
import sys
assert ('linux' in sys.platform), "該代碼只能在 Linux 下執行"
# 接下來要執行的代碼
七、小結 (summary)
本文中主要說明的 Python 異常相關常見關鍵字 (還有 assert 用於斷言):
八、彩蛋 —— finally 和 return 誰的優先級更高?(選讀)
已知 try-finally 語句中,無論 try 子句正常執行還是引發異常,finally 子句最終都能被執行 (用於收尾)。又知, return 作爲函數的出口,每逢 return 語句,函數都將結束運行。那麼問題來了,如果 同時存在 finally 和 return,誰的優先級更高,Python 解釋器將如何抉擇?測試一下:
>>> def test(): # 在 try 子句與 finally 子句均書寫 return 語句
try:
return "try"
finally:
return "finally"
>>> test()
'finally'
可見,Python 解釋器忽略了 try 子句中的 return 語句 (此處的 return 並非函數終點),以確保 finally 子句的執行。
但其實,try 子句中的 return 語句並非被忽視。已知函數未顯式定義 return 語句時,將隱式地返回 None (返回值爲 None)。那麼,若 finally 子句中未顯式定義 return 語句時,是否應返回 None 呢?驗證一下:
>>> def val(): # 只在 try 子句中書寫 return 語句
try:
return "try"
finally:
... # ... 等同於 pass
>>> val()
'try'
# ------------------------------------------------------------------------------
>>> def val(): # 只在 try 子句中書寫 return 語句
try:
return "try"
finally:
print("finally")
>>> val()
finally
'try'
# ------------------------------------------------------------------------------
>>> def val(): # 在 try 子句與 finally 子句中均不書寫 return 語句
try:
print("try")
finally:
print("finally")
>>> val()
try
finally
可見,未在 finally 子句中顯式定義 return 語句時,try 子句中的 return 語句還是有效的。
總而言之,在包含 try-finally 語句的函數中:若 finally 子句中顯式定義了 return 語句,那麼該 return 語句會直接覆蓋 try 子句中的 return 語句 (如果有);若 finally 子句中未顯式定義 return 語句,那麼 try 子句中的 return 語句 (如果有) 將生效。
參考文獻:
《Think Python》、《Python Immediate》
https://docs.python.org/zh-cn/3.6/tutorial/errors.html?highlight=異常
https://docs.python.org/zh-cn/3.6/library/exceptions.html?highlight=異常
https://www.runoob.com/python3/python3-errors-execptions.html
https://www.runoob.com/python/python-exceptions.html
https://baijiahao.baidu.com/s?id=1630856225859243901&wfr=spider&for=pc