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