Python 例外狀況介紹,以及擲回和擷取 Python 例外狀況

閱讀 26:39·字數 7998·發佈 
Youtube 頻道
訂閱 133

先決條件

閱讀本節的先決條件是已經掌握例外狀況的相關概念,你可以檢視程式設計教學例外狀況,例外狀況處理介紹一節來了解他們。

Python 例外狀況

在 Python 中,所有例外狀況(表示例外狀況的類別)都需要繼承自BaseExceptionException,這包括 Python 的內建例外狀況,以及由開發人員定義的例外狀況。當然,只有少數 Python 例外狀況直接繼承自類別BaseException,比如,可導致 Python 解譯器(可以簡單的理解為 Python 的可執行檔案或程式)結束的例外狀況SystemExit,剩余 Python 例外狀況均繼承自類別Exception或類別Exception的衍生類別。

Python 例外狀況基底類別 BaseException 和 Exception 之間的區別

ExceptionBaseException類別的衍生類別,他表示不是來自於系統的非正常情況,比如,表示除數為0的 Python 例外狀況ZeroDivisionError。一般情況下,開發人員僅需要擷取從Exception類別衍生的各種 Python 例外狀況,如果將擷取的範圍擴大到BaseException,那麽可能會導致一些意想不到的問題,比如,在try…except中執行的陳述式sys.exit()無法實作結束 Python 解譯器的效果。

在下面的範例中,由於我們將擷取範圍設定為所有 Python 例外狀況,因此sys模組的exit函式所產生的例外狀況SystemExit,並不會導致 Python 解譯器的結束,最後的print函式將被執行。

base.py
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方法建置一個有用的註解。

custom.py
# 定義一個新的例外狀況 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陳述式將被忽略。

在下面的範例中,由於例外狀況ExceptionZeroDivisionError的基底類別,因此,第一個except陳述式與被引發的 Python 例外狀況相符,第二個except陳述式將被忽略,即便他與被引發的 Python 例外狀況同樣相符。

try.py
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.py
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 例外狀況。

as.py
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陳述式比對例外狀況TypeErrorAttributeError,第二個except陳述式比對其他所有例外狀況。

except.py
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陳述式來處理這種情況,並將一部分tryexcept關鍵字之間的程式碼,轉移到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陳述式。

else.py
try:
	import random
	# 有一定的幾率引發例外狀況 Exception
	if random.randint(0, 1):
		raise Exception()
except:
	# 至少需要一個 except,才能書寫 else
	print('哎呀!一個錯誤')
else:
	# 僅在沒有例外狀況時執行
	print('居然沒有錯誤!')
# 輸出結果是隨機的
居然沒有錯誤!

執行 try 陳述式相關程式碼中的跳躍陳述式將導致 else 陳述式被忽略

try陳述式的相關程式碼中的returncontinuebreak陳述式被執行時,else陳述式將被忽略,即便整個try陳述式的相關程式碼沒有引發任何 Python 例外狀況。

函式run中的print函式不會被呼叫,因為try陳述式之後的return陳述式將被執行。

else.py
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 陳述式的相關程式碼將在其他跳躍陳述式執行之前執行

tryexceptelse陳述式的相關程式碼中存在某些跳躍陳述式時,比如breakcontinuereturn,與finally陳述式相關的程式碼將在這些跳躍陳述式執行之前被執行。

finally.py
def show():
	try:
		# 傳回之前會執行 finally 中的程式碼
		return '一條訊息'
	finally:
		# 在真正傳回之前,這裏的程式碼將被執行
		print(f'在傳回之前執行!')

print(show())
在傳回之前執行!
一條訊息

finally 陳述式相關程式碼中的 return 陳述式的傳回值將取代其他傳回值

如果finally陳述式的相關程式碼中包含了return陳述式,那麽該return陳述式所傳回的值(包括空值None),將取代tryexceptelse陳述式相關程式碼中的傳回值。

在下面的程式碼中,呼叫div(2, 0)將引發 Python 例外狀況ZeroDivisionError,但陳述式return 0並不能讓函式的傳回值成為0,因為finally包含了自己的return陳述式,div(2, 0)的最終傳回值將是2.0

finally.py
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的陳述式(假設erras關鍵字繫結的識別碼),他們會展示不同的回溯(Traceback)資訊。

在下面的範例中,陳述式raise NameError等同於陳述式raise NameError()except之後的陳述式raise,會將已經擷取到的型別為NameError的例外狀況重新擲回。

raise.py
try:
	# 擲回例外狀況 NameError
	raise NameError
except NameError:
	# 在比對例外狀況之後,重新將其擲回
	raise
NameError

finally 陳述式相關程式碼中的跳躍陳述式將使未處理的 Python 例外狀況不再重新擲回

如果finally陳述式的相關程式碼中包含了跳躍陳述式,比如breakcontinuereturn,那麽這些跳躍陳述式的執行,將導致未被except處理的 Python 例外狀況不再被重新擲回,即便這些例外狀況是通過raise陳述式主動擲回的。

3.8 版本之前,在 Python 的finally陳述式的相關程式碼中,不能使用continue陳述式。

在下面的程式碼中,如果函式no_exception中的return陳述式被執行,那麽未處理的型別為AttributeError的 Python 例外狀況將不再被重新擲回。

no_exception.py
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 陳述式的相關程式碼執行完畢後才能重新擲回

tryexceptelse陳述式的相關程式碼引發新的不能被處理的 Python 例外狀況時,這些例外狀況不會被立即擲回,他們需要等待finally陳述式的相關程式碼的執行。

在下面的程式碼中,型別為ValueError的例外狀況將被except陳述式處理,而except之後的raise陳述式引發了新的 Python 例外狀況,由於新的例外狀況無法得到處理,他會在finally陳述式的相關程式碼執行完畢之後,被重新擲回。

wait.py
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 例外狀況。

from.py
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 例外狀況的資訊。

from.py
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,回溯中顯示了更少的資訊。

from_none.py
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

內容分類

程式碼

src/zh-hant/exceptions·codebeatme/python·GitHub