Python 例外狀況介紹,以及擲回和擷取 Python 例外狀況
先決條件
閱讀本節的先決條件是已經掌握例外狀況的相關概念,你可以檢視程式設計教學的例外狀況,例外狀況處理介紹一節來了解他們。
Python 例外狀況
在 Python 中,所有例外狀況(表示例外狀況的類別)都需要繼承自BaseException
或Exception
,這包括 Python 的內建例外狀況,以及由開發人員定義的例外狀況。當然,只有少數 Python 例外狀況直接繼承自類別BaseException
,比如,可導致 Python 解譯器(可以簡單的理解為 Python 的可執行檔案或程式)結束的例外狀況SystemExit
,剩余 Python 例外狀況均繼承自類別Exception
或類別Exception
的衍生類別。
Python 例外狀況基底類別 BaseException 和 Exception 之間的區別
Exception
是BaseException
類別的衍生類別,他表示不是來自於系統的非正常情況,比如,表示除數為0
的 Python 例外狀況ZeroDivisionError
。一般情況下,開發人員僅需要擷取從Exception
類別衍生的各種 Python 例外狀況,如果將擷取的範圍擴大到BaseException
,那麽可能會導致一些意想不到的問題,比如,在try…except
中執行的陳述式sys.exit()
無法實作結束 Python 解譯器的效果。
在下面的範例中,由於我們將擷取範圍設定為所有 Python 例外狀況,因此sys
模組的exit
函式所產生的例外狀況SystemExit
,並不會導致 Python 解譯器的結束,最後的print
函式將被執行。
import sys
try:
sys.exit()
except BaseException as e:
# 例外狀況 SystemExit 將被擷取
print(f'擷取到了例外狀況 {type(e)}')
# 下面的陳述式會被執行
print('sys.exit() 似乎沒有作用哦!')
擷取到了例外狀況 <class 'SystemExit'>
sys.exit() 似乎沒有作用哦!
自訂 Python 例外狀況
雖然 Python 已經提供了足夠多的內建例外狀況,但開發人員可能依然希望能夠自己定義他們,以完成一些特殊的業務邏輯,比如,定義一個名稱為NicknameError
的例外狀況,用於說明使用者昵稱不正確的情況。這些自訂的 Python 例外狀況應該繼承自類別Exception
或其衍生類別,而不是直接繼承BaseException
,原因在於直接繼承自BaseException
的例外狀況通常用於表示來自於系統的非正常情況。
無論是繼承自哪個類別,你所定義的 Python 例外狀況均擁有一個具有可變參數args
的建構子,這表示可以使用任意的位置參數來建立 Python 例外狀況的執行個體。
如何為自訂 Python 例外狀況命名?
在 Python 中,大部分內建例外狀況的名稱都以Error
結尾,開發人員自訂的 Python 例外狀況應該遵守此約定。
如何在 Python 例外狀況的標準回溯中新增註解?
在 3.11 或更高的版本中,Python 例外狀況擁有一個名稱為add_note
的方法,可用於為例外狀況的標準回溯新增註解資訊,這些資訊可作為例外狀況的更進一步說明。
一旦add_note
方法被呼叫,名稱為__notes__
的 Python 串列將被定義在例外狀況中,他包含了所有通過add_note
方法新增的字串。
類別繼承
想要了解更多關於 Python 類別繼承的內容,你可以檢視Python 類別繼承介紹,以及實作 Python 類別繼承,多重繼承,方法覆寫一節。
下面,我們定義了一個新的例外狀況NicknameError
,並將第一和第二個位置參數規定為使用者昵稱和提示資訊,以通過add_note
方法建置一個有用的註解。
# 定義一個新的例外狀況 NicknameError
class NicknameError(Exception):
def __init__(self, *args):
super().__init__(*args)
# 將第一個和第二個位置參數作為使用者昵稱和提示資訊
(nickname, msg) = args
# 為例外狀況新增註解
self.add_note(f'糟糕,昵稱“{nickname}”出問題了!{msg}')
print(self.__notes__)
# 擲回例外狀況 NicknameError
raise NicknameError('Test', '不被允許的昵稱')
['糟糕,昵稱“Test”出問題了!不被允許的昵稱']
NicknameError: ('Test', '不被允許的昵稱')
糟糕,昵稱“Test”出問題了!不被允許的昵稱
使用 try…except 陳述式擷取處理 Python 例外狀況
對於可能引發例外狀況的 Python 程式碼,你可以使用try…except
陳述式來擷取並處理例外狀況,只需要將這些 Python 程式碼放置在try
陳述式之後,並使用except
陳述式來比對例外狀況即可。
try:
<try-block>
except <exception-1>:
<except-block-1>
…
except <exception-N>:
<except-block-N>
- try-block 部分
try-block
為可能會引發例外狀況的 Python 程式碼(需要使用空白字元進行縮排),當某個陳述式擲回例外狀況後,try-block
中剩余的陳述式將不再被執行。- exception 部分
exception
為用於比對特定例外狀況的運算式,需要給出一個或多個 Python 例外狀況型別,當被擲回的例外狀況或其基底類別屬於這些型別時,表示比對成功。- except-block 部分
except-block
為處理例外狀況的程式碼(需要使用空白字元進行縮排),當被擲回的 Python 例外狀況與某一個except
陳述式相符時,except
陳述式對應的except-block
部分的程式碼將被執行。
最多只有一個 except 陳述式能與被擲回 Python 例外狀況相符
如果try…except
陳述式擁有多個except
陳述式,那麽與 Python 的if
陳述式類似,他們會按照先後順序進行比對,當某一個except
陳述式與被擲回的 Python 例外狀況相符時,其余的except
陳述式將被忽略。
在下面的範例中,由於例外狀況Exception
是ZeroDivisionError
的基底類別,因此,第一個except
陳述式與被引發的 Python 例外狀況相符,第二個except
陳述式將被忽略,即便他與被引發的 Python 例外狀況同樣相符。
try:
# 除數為 0 將引發例外狀況 ZeroDivisionError
num = 1 / 0
except Exception:
# Exception 是 ZeroDivisionError 的基底類別,這裏的程式碼會被執行
print('比對到了例外狀況 ZeroDivisionError')
except ZeroDivisionError:
print('無人問津!')
比對到了例外狀況 ZeroDivisionError
與所有 except 陳述式均不相符的 Python 例外狀況將被重新擲回
如果一個 Python 例外狀況被引發,並且與所有的except
陳述式均不相符,那麽該例外狀況將作為未處理的 Python 例外狀況被重新擲回,這可能導致整個程式因此結束。相反的,如果例外狀況與某個except
陳述式相符,那麽他將被視為已處理,已處理的 Python 例外狀況不會被重新擲回。
在下面的範例中,IOError
並非NameError
的基底類別,這導致例外狀況沒有被處理,他會被重新擲回並最終顯示了一些錯誤資訊。
try:
# 存取未定義的 undefined,將引發例外狀況 NameError
print(undefined)
except IOError:
# IOError 不是 NameError 的基底類別,這裏的程式碼不會執行
print('出現了 IO 錯誤?不可能')
NameError: name 'undefined' is not defined
在 except 陳述式的相關程式碼中參考目前被處理的 Python 例外狀況
在except
陳述式中,使用as
關鍵字指定一個與被處理例外狀況繫結的識別碼(可將其簡單的視為 Python 變數),即可通過該識別碼在except
陳述式相關的程式碼中存取被處理的 Python 例外狀況。比如,IOError as err
,(TypeError,AttributeError) as e
。
此外,在 3.11 或更高版本中,通過sys
模組的exception
函式,同樣可在except
陳述式相關的程式碼中存取目前被處理的 Python 例外狀況。
except 陳述式會刪除使用 as 關鍵字與 Python 例外狀況繫結的識別碼
如果你在某個except
陳述式中使用了as
關鍵字,那麽as
關鍵字指定的識別碼,將在該except
陳述式的相關程式碼執行完畢時被刪除。這意味著識別碼僅在except
陳述式中保持其正確性,他不應該與同一命名空間的其他識別碼重複,以避免一些不必要的錯誤。
例外狀況
想要了解命名空間這一概念,你可以檢視程式設計教學的例外狀況,例外狀況處理介紹一節。
下面的程式碼定義了變數err
,他同時是except
陳述式繫結的識別碼,因此在except
陳述式的相關程式碼執行完畢後,err
將成為未定義的內容,使用print
函式來顯示他會導致錯誤。
一旦脫離了except
陳述式,sys
模組的函式exception
將傳回空值None
,因為此時不存在正被處理的 Python 例外狀況。
import sys
# 該變數稍後會被刪除
err = 'error'
try:
# 除數為 0 將引發例外狀況 ZeroDivisionError
1 / 0
except ZeroDivisionError as err:
# 通過識別碼 err 和 exception 函式取得的例外狀況物件相同
print(sys.exception() == err)
print(sys.exception())
print(err)
True
None
…
NameError: name 'err' is not defined
使用 except 陳述式比對多個或任意型別的 Python 例外狀況
在多數情況下,一個except
陳述式只處理一種特定型別的 Python 例外狀況,但這不排除一個except
陳述式處理多種特定型別的例外狀況的可能,要完成此目標,你可以在exception
部分使用元組包含多個例外狀況型別,比如,(TypeError,AttributeError)
。
除了比對多個 Python 例外狀況型別,except
陳述式同樣可以比對任意 Python 例外狀況型別,這需要你將exception
部分留空,即不書寫任何運算式。
比對任意 Python 例外狀況型別的 except 陳述式必須是最後一個 except 陳述式
當一個except
陳述式比對任意型別的 Python 例外狀況時,該except
陳述式必須是最後一個except
陳述式,原因很簡單,如果他位於其他except
陳述式之前,那麽一些except
陳述式將失去被執行的可能。也許,Python 可以從底層打亂except
陳述式的執行順序,但那樣的行為似乎並不明智。
下面的第一個except
陳述式比對例外狀況TypeError
或AttributeError
,第二個except
陳述式比對其他所有例外狀況。
import random
import sys
try:
# 從多個例外狀況中隨機選擇一個並引發
errs = [TypeError, AttributeError, ZeroDivisionError, IndexError]
err = errs[random.randint(0, 3)]
raise err()
except (TypeError, AttributeError) as e:
# 檢視例外狀況的具體型別
print(f'例外狀況 {type(e)}')
except:
# 既不是 TypeError 也不是 AttributeError 的例外狀況
print(f'其他例外狀況 {type(sys.exception())}')
# 輸出結果是隨機的
其他例外狀況 <class 'IndexError'>
使用 try…except…else 陳述式處理沒有 Python 例外狀況被擲回(引發)的情況
被try…except
陳述式包括的 Python 程式碼,可能會正確執行而不引發任何例外狀況,你可以使用try…except…else
陳述式來處理這種情況,並將一部分try
和except
關鍵字之間的程式碼,轉移到else
陳述式之後,這會使得被轉移的 Python 程式碼不再參與例外狀況的擷取,他們僅在沒有任何 Python 例外狀況被引發時執行。
try:
<try-block>
except <exception-1>:
<except-block-1>
…
except <exception-N>:
<except-block-N>
else:
<else-block>
- else-block 部分
else-block
為沒有 Python 例外狀況被引發時執行的程式碼(需要使用空白字元進行縮排)。
使用 else 陳述式的先決條件是至少擁有一個 except 陳述式
如果要使用else
陳述式來處理沒有 Python 例外狀況被引發的情況,那麽在try
陳述式之後,需要書寫至少一個except
陳述式。
try:
import random
# 有一定的幾率引發例外狀況 Exception
if random.randint(0, 1):
raise Exception()
except:
# 至少需要一個 except,才能書寫 else
print('哎呀!一個錯誤')
else:
# 僅在沒有例外狀況時執行
print('居然沒有錯誤!')
# 輸出結果是隨機的
居然沒有錯誤!
執行 try 陳述式相關程式碼中的跳躍陳述式將導致 else 陳述式被忽略
當try
陳述式的相關程式碼中的return
,continue
或break
陳述式被執行時,else
陳述式將被忽略,即便整個try
陳述式的相關程式碼沒有引發任何 Python 例外狀況。
函式run
中的print
函式不會被呼叫,因為try
陳述式之後的return
陳述式將被執行。
def run():
try:
# 執行 return 將導致 else 陳述式被忽略
return
except:
pass
else:
# 這裏的陳述式不會被執行
print('太好了,沒有錯誤')
run()
使用 finally 陳述式完成 Python 例外狀況處理的清理工作
在處理 Python 例外狀況的過程中,一些程式碼需要始終被執行,無論是否有 Python 例外狀況被擲回,或 Python 例外狀況是否被處理。使用finally
陳述式可以達成上述目標,該陳述式之後的程式碼通常與清理工作有關,比如,關閉開啟的檔案。
try:
<try-block>
…
finally:
<finally-block>
- finally-block 部分
finally-block
為始終被執行的 Python 程式碼(需要使用空白字元進行縮排)。
使用 finally 陳述式後 except 陳述式將成為選擇性的
如果書寫了finally
陳述式,那麽except
陳述式將不再是必須的,try
陳述式之後可以沒有任何except
陳述式。
finally 陳述式的相關程式碼將在其他跳躍陳述式執行之前執行
當try
,except
或else
陳述式的相關程式碼中存在某些跳躍陳述式時,比如break
,continue
和return
,與finally
陳述式相關的程式碼將在這些跳躍陳述式執行之前被執行。
def show():
try:
# 傳回之前會執行 finally 中的程式碼
return '一條訊息'
finally:
# 在真正傳回之前,這裏的程式碼將被執行
print(f'在傳回之前執行!')
print(show())
在傳回之前執行!
一條訊息
finally 陳述式相關程式碼中的 return 陳述式的傳回值將取代其他傳回值
如果finally
陳述式的相關程式碼中包含了return
陳述式,那麽該return
陳述式所傳回的值(包括空值None
),將取代try
,except
或else
陳述式相關程式碼中的傳回值。
在下面的程式碼中,呼叫div(2, 0)
將引發 Python 例外狀況ZeroDivisionError
,但陳述式return 0
並不能讓函式的傳回值成為0
,因為finally
包含了自己的return
陳述式,div(2, 0)
的最終傳回值將是2.0
。
def div(a, b):
try:
# 如果除數為 0,則引發例外狀況 ZeroDivisionError
return a / b
except ZeroDivisionError:
# 這裏的傳回值將被忽略
return 0
finally:
# 這裏是最終的傳回值
return a / (b + 1)
print(div(2, 0))
2.0
使用 raise 陳述式擲回(引發)Python 例外狀況
使用 Python 提供的raise
陳述式,開發人員可以主動擲回(引發)一個 Python 例外狀況,其基本的語法形式如下。
raise <exception>
- exception 部分
exception
是被擲回的 Python 例外狀況物件,或 Python 例外狀況型別。如果僅給出例外狀況型別,那麽將根據該型別隱含建立其對應的執行個體,比如,raise NameError
的效果等同於raise NameError()
。
如何使用 raise 陳述式擲回(引發)目前被處理的 Python 例外狀況?
在except
陳述式的相關程式碼中,你可以將raise
陳述式的exception
部分留空,這會將目前被處理的 Python 例外狀況重新擲回。
需要指出的是,以上做法的效果並不等同於呼叫exception
函式的陳述式raise sys.exception()
,或類似於raise err
的陳述式(假設err
為as
關鍵字繫結的識別碼),他們會展示不同的回溯(Traceback)資訊。
在下面的範例中,陳述式raise NameError
等同於陳述式raise NameError()
,except
之後的陳述式raise
,會將已經擷取到的型別為NameError
的例外狀況重新擲回。
try:
# 擲回例外狀況 NameError
raise NameError
except NameError:
# 在比對例外狀況之後,重新將其擲回
raise
NameError
finally 陳述式相關程式碼中的跳躍陳述式將使未處理的 Python 例外狀況不再重新擲回
如果finally
陳述式的相關程式碼中包含了跳躍陳述式,比如break
,continue
或return
,那麽這些跳躍陳述式的執行,將導致未被except
處理的 Python 例外狀況不再被重新擲回,即便這些例外狀況是通過raise
陳述式主動擲回的。
3.8 版本之前,在 Python 的finally
陳述式的相關程式碼中,不能使用continue
陳述式。
在下面的程式碼中,如果函式no_exception
中的return
陳述式被執行,那麽未處理的型別為AttributeError
的 Python 例外狀況將不再被重新擲回。
def no_exception(r):
try:
# 引發例外狀況 AttributeError
raise AttributeError()
finally:
if r:
print('呼叫了 return 陳述式')
# 這裏的 return 陳述式將導致未處理的例外狀況不被擲回
return
else:
print('沒有呼叫 return 陳述式')
# 不會顯示錯誤資訊
no_exception(True)
# 會顯示錯誤資訊
no_exception(False)
呼叫了 return 陳述式
沒有呼叫 return 陳述式
…
AttributeError
未被處理的 Python 例外狀況需要在 finally 陳述式的相關程式碼執行完畢後才能重新擲回
當try
,except
或else
陳述式的相關程式碼引發新的不能被處理的 Python 例外狀況時,這些例外狀況不會被立即擲回,他們需要等待finally
陳述式的相關程式碼的執行。
在下面的程式碼中,型別為ValueError
的例外狀況將被except
陳述式處理,而except
之後的raise
陳述式引發了新的 Python 例外狀況,由於新的例外狀況無法得到處理,他會在finally
陳述式的相關程式碼執行完畢之後,被重新擲回。
try:
# 擲回例外狀況 ValueError
raise ValueError
except ValueError:
# 擲回新的例外狀況 NameError,該例外狀況無法被處理
raise NameError
finally:
print('執行 finally 中的程式碼後,才能看到錯誤資訊')
執行 finally 中的程式碼後,才能看到錯誤資訊
…
ValueError
…
NameError
使用 raise…from 陳述式將已處理的 Python 例外狀況指定為原因
如果一個 Python 例外狀況已經被某個except
陳述式處理,而該except
陳述式的相關程式碼引發了新的例外狀況,那麽新的 Python 例外狀況的__context__
變數,即例外狀況的內容,將指向已被處理的 Python 例外狀況,以表示他們之間的關聯。
在下面的範例中,except
使用raise
陳述式引發了新的型別為ValueError
的例外狀況,他的__context__
變數將指向已被except
處理的型別為ZeroDivisionError
的 Python 例外狀況。
try:
try:
# 引發例外狀況 ZeroDivisionError
1 / 0
except:
# 處理 ZeroDivisionError 之後,引發新的例外狀況 ValueError
raise ValueError
except Exception as err:
print(f'{type(err)} 的 __context__ 的型別為 {type(err.__context__)}')
<class 'ValueError'> 的 __context__ 的型別為 <class 'ZeroDivisionError'>
如果新的 Python 例外狀況是通過raise…from
陳述式引發,那麽你可以為新的 Python 例外狀況指定一個表示原因的例外狀況,這通常說明新的 Python 例外狀況是由該例外狀況導致的。
raise…from
陳述式可以在其他位置使用,但如果位於except
陳述式的相關程式碼中,那麽表示原因的例外狀況一般被指定為已被except
處理的 Python 例外狀況。
raise <newexception> from <causeexception>
- newexception 部分
newexception
是被擲回的 Python 例外狀況物件,或 Python 例外狀況型別。如果僅給出例外狀況型別,那麽將根據該型別隱含建立其對應的執行個體,比如,raise RuntimeError from ValueError()
的效果等同於raise RuntimeError() from ValueError()
。- causeexception 部分
causeexception
是表示原因的 Python 例外狀況物件,或 Python 例外狀況型別。如果僅給出例外狀況型別,那麽將根據該型別隱含建立其對應的執行個體,比如,raise RuntimeError() from ValueError
的效果等同於raise RuntimeError() from ValueError()
。
回溯將優先展示表示原因的 Python 例外狀況的資訊
一旦使用了raise…from
陳述式,被擲回的例外狀況的__cause__
變數將指向表示原因的 Python 例外狀況,__suppress_context__
變數將被設定為True
,已明確的指示應在回溯中采用__cause__
而非__context__
,即展示表示原因的 Python 例外狀況的資訊,而不是表示內容的 Python 例外狀況的資訊,除非__cause__
變數為None
並且__suppress_context__
變數為False
。
在下面的範例中,我們使用raise…from
陳述式將原因指定為型別為ValueError
的例外狀況,而不是except
陳述式已經處理的例外狀況,這導致回溯中不再展示型別為ZeroDivisionError
的例外狀況的相關資訊。
這裏需要指出,雖然程式碼將例外狀況的__suppress_context__
變數設定為False
,但由於其__cause__
不為None
,因此,回溯中不會顯示表示內容的 Python 例外狀況的資訊。
try:
try:
# 引發例外狀況 ZeroDivisionError
1 / 0
except:
# 引發例外狀況 RuntimeError,但並未將 ZeroDivisionError 作為原因
raise RuntimeError from ValueError
except Exception as err:
print(type(err.__context__))
print(type(err.__cause__))
print(err.__suppress_context__)
err.__suppress_context__ = False
# 重新擲回例外狀況
raise
<class 'ZeroDivisionError'>
<class 'ValueError'>
True
ValueError
…
The above exception was the direct cause of the following exception:
…
RuntimeError
在 raise…from 陳述式中將 None 指定為原因
raise…from
陳述式的causeexception
部分可以是空值None
,這將使得被擲回的 Python 例外狀況的__cause__
變數為None
,__suppress_context__
變數為True
,回溯中顯示的資訊被簡化。
在下面的範例中,我們使用raise…from
陳述式將原因指定為空值None
,回溯中顯示了更少的資訊。
try:
try:
# 引發例外狀況 ZeroDivisionError
1 / 0
except:
# 引發例外狀況 RuntimeError,但將原因設定為 None
raise RuntimeError from None
except Exception as err:
print(type(err.__context__))
print(err.__cause__)
print(err.__suppress_context__)
# 重新擲回例外狀況
raise
<class 'ZeroDivisionError'>
None
True
…
RuntimeError