Python 例外狀況介紹,以及擲回和擷取例外狀況
訂閱 375
先決條件
閱讀本節的先決條件是已經掌握例外狀況的相關概念,你可以檢視程式設計教學的例外狀況,例外狀況處理介紹一節來了解他們。
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:
# 在比對例外狀況之後,重新將其擲回
raise
NameError
finally 中的跳躍陳述式將使未處理的例外狀況不再重新擲回
如果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