【Python】詳解 try-except-else-finally 語句 —— 異常處理完全解讀(上)

目錄

一、緒論 (introduction)

二、異常捕獲 (exception catching)

2.1 try-except 語句

2.1.1 基本用法

2.1.2 指定異常類型

2.1.3 小結

2.2 try-except-else 語句

2.3 try-except-else-finally 語句

2.4 小結

三、異常拋出 (exception raising)

四、異常自定義 (exception customizing)

五、預定義的清理行爲 (with statement)

六、斷言 (asserting)

七、小結 (summary)

八、彩蛋 —— 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 語句 代碼塊是最基本的 異常捕獲和處理 方法,其常見的搭配形式 (關鍵詞組合) 有:

  1. try-except
  2. try-except-else
  3. 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

https://www.runoob.com/python3/python3-assert.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章