使用者:Xyy23330121/Python/處理異常


我們之前已經接觸到一些報錯了。比如在可變序列操作表內,就列出了許多會因為「輸入的數據與函數需求不符」導致的報錯。

這些報錯除了在試運行程序時,可以簡潔直白地說明錯誤位置和錯誤原因。通過以下的內容,還可以用作代碼的一部分。

try 語句

編輯

try 語句會先執行 try 子句,如果沒有觸發異常,則跳過 except 子句。如果觸發異常,則會跳過 try 子句剩下的部分,執行 except 子句。

我們看以下示例:

while True:
    try:
        x = float(input("请输入一个数字: "))
        break
    except:
        print("请重新输入。")

該示例會要求用戶一直輸入內容,直到輸入有效的數字為止。

匹配異常

編輯
異常類型
一切引發的異常,都是異常類
或異常類的實例。

我們可以在 except 後面添加異常的類型,來匹配異常。

對於一個 except 語句。當引發的異常不是 except 右側給出的異常類型(或其子類)的實例時,跳過該 except 語句。

當引發的異常是右側異常類型的子類,或異常類型元組中某個類型的子類時,會執行該 except 子句,並跳過之後的所有的 except 語句。

我們看以下示例:

while True:
    try:
        x = float(input("请输入一个数字: "))
        break
    except ValueError:
        #当 try 子句中引发的异常是ValueError或其子类的实例时,执行以下语句。
        print("抱歉,输入的并不是有效的数字,请重新输入。")
    except (AttributeError, TypeError):
        #当 try 子句中引发的异常是AttributeError或TypeError或两者子类的实例时,执行以下语句。
        pass
    except Exception:
        #只要引发可处理的异常,就执行以下语句。
        pass

和以下示例:

while True:
    try:
        x = float(input("请输入一个数字: "))
        break
    except ValueError:
        #当 try 子句中引发的异常是ValueError或其子类的实例时,执行以下语句。
        print("抱歉,输入的并不是有效的数字,请重新输入。")
    except Exception:
        #只要引发异常,执行以下语句。
        pass
    except (AttributeError, TypeError):
        #以下语句不会被执行,因为以上AttributeError和TypeError都是Exception的子类。
        #总是会先匹配到 Exception 并跳过对 AttributeError 和 TypeError 的匹配。
        pass

以上的 except Exception:except: 是完全等同的。

獲取異常類型

編輯
try:
    raise Exception
except Exception as e:
    print(type(e))  #输出:<class 'Exception'>

利用 as 關鍵詞,我們可以將所引發的異常賦值給其後的變量名,並在處理異常時訪問關於異常的信息。

raise 主動引發異常

編輯

我們可以用 raise 語句主動引發異常。比如以下示例:

raise Exception
print("由于已经引发异常,本内容不会被输出。")

其輸出為:

Traceback (most recent call last):
  ...
    raise Exception
Exception

raise 語句中,raise 後面跟着的是一個異常類型,或異常類型的實例。

對於大多數異常類型,我們可以創建帶有注釋的實例。比如:

a = Exception('注释')
raise a

這在許多情況下很有用。

except: raise

編輯

特別的,如果在 except 子句中,可以這樣使用 raise 語句:

try:
    1/0
except:
    print('引发了错误')
    raise

其輸出為:

引发了错误
Traceback (most recent call last):
  ...
    1/0
    ~^~
ZeroDivisionError: division by zero

此時,raise 語句會原封不動地將正在處理的錯誤重新引發一遍。

raise from

編輯

為了表示一個異常是另一個異常的直接後果,可以用這種方法:

raise CausedException from Exception

比如以下示例:

try: 1 / 0
except ZeroDivisionError as e:
    raise ValueError("除数不能为零") from e

其輸出為:

Traceback (most recent call last):
  ...
    try: 1 / 0
         ~~^~~
ZeroDivisionError: division by zero

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  ...
    raise ValueError("除数不能为零") from e
ValueError: 除数不能为零

異常鏈

編輯

如果在處理異常時,又引發一個異常,則會將正在處理的異常加入到新異常的注釋中,比如:

try: 1 / 0
except ZeroDivisionError:
    raise ValueError

其輸出為:

Traceback (most recent call last):
  ...
    try: 1 / 0
         ~~^~~
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  ...
    raise ValueError
ValueError

raise from None

編輯

作為 raise 語句的特殊用法。如果使用 raise Exception from None,則會禁用異常鏈。

try: 1 / 0
except ZeroDivisionError:
    raise ValueError from None

其輸出為:

Traceback (most recent call last):
  ...
    raise ValueError from None
ValueError

異常類型

編輯

一切引發的異常,都是異常類或異常類的實例。而一切異常類型都是 BaseException 的子類。

由於 BaseException 的一些子類包含「系統退出」等異常——此類異常一旦引起,Python 必須退出,因此不能對該類異常進行處理,也不應在代碼中匹配 BaseException 類。

可處理的異常的基類是 Exception 。所有內置的非系統退出類的異常都是該類的子類。所有用戶自定義異常也應當是該類的子類。

異常的屬性與方法

編輯
  1. args
    包含錯誤信息的元組。對於一般的異常,該元組只包含一個給出錯誤信息的字符串。對於一些特殊的內置異常,比如 OSError,該元組中的元素可以有特殊的含義。
  2. with_traceback(traceback)
    該方法為異常提供了內置異常中,提示錯誤位置的部分。
  3. __traceback__
    with_traceback方法中,用於存儲該方法提示的屬性。
  4. add_note(note)
    該方法向該異常的注釋列表添加注釋。該方法一般由 Python 在創建「異常鏈」時自動調用,讀者不必掌握該方法的內容。
  5. __notes___
    異常的注釋列表。

創建異常對象

編輯

我們可以通過e = Exception(*args)方法創建異常。其中傳入args的內容會原封不動放置到 e.args 裡面。

traceback

編輯

我們一般不手動創建 traceback 對象,而是在引發錯誤時提取錯誤的 traceback 對象並進行使用。比如:

import sys
try:
    1/0
except:
    print('引发了错误')
    tb = sys.exception().__traceback__
    raise Exception().with_traceback(tb) from None

自定義異常類型

編輯

我們之前已經可以自定義異常的屬性,現在我們可以自定義異常的類型。

自定義異常類型的方法和一般的類型是一致的,不過要求它必須繼承自Exception,比如:

class MyException(Exception): pass
class MyException1(ZeroDivisionError): pass

異常的提示功能

編輯

對於大多數情況,引發任何內置異常的或自定義的異常,都足以滿足程序邏輯的需求。但隨意引發的異常對於修復異常是沒有幫助的。

讀者在引發異常或自定義異常時,應注意提供詳實的異常信息,並使用正確的內置異常、或自定義一個明確的異常名稱。

finally 與 else

編輯

try 語句的結構中,還可以添加 finally 語句和 else 語句。其順序可以如下表示:

try: pass
except: pass
else: pass
finally: pass

try 子句沒有引發任何異常時,如果有 else 語句,則會執行其子句。

finally

編輯

無論之前異常的處理情況如何,finally 子句都會被執行。具體來講:

  1. 如果所有異常都被匹配並處理,則正常執行 finally 子句。
  2. 如果有異常未被處理,則執行 finally 子句,然後再引發異常。

比如:

try:
    1/0
except IndexError:
    pass
finally:
    print("先执行finally,再引发异常")
先执行finally再引发异常
Traceback (most recent call last):
  ...
    1/0
    ~^~
ZeroDivisionError: division by zero

異常組與 except*

編輯

except* 語句用於處理異常組。

異常組

編輯

異常組是一種特殊的異常。其創建方法為:

ExceptionGroup(msg, excs)

其中,msg 是一個包含錯誤信息的字符串,而 excs 是包含多個異常的可迭代序列。excs 中的異常只能為 Exception 的子類。

類似 ExceptionBaseException,也有 BaseExceptionGroup。與 ExceptionGroup 不同的是,BaseExceptionGroup 中可以包含 BaseException 的子類。

匹配方法

編輯
try:
    raise ExceptionGroup("异常组",
     [ValueError("错误:ValueError"),
      TypeError("错误:TypeError1"),
      TypeError("错误:TypeError2")])
except* TypeError as e:
    print(f'捕捉到{e.exceptions}')
except* ValueError as e:
    print(f'捕捉到{e.exceptions}')

輸出為:

捕捉到(TypeError('错误:TypeError1'), TypeError('错误:TypeError2'))
捕捉到(ValueError('错误:ValueError'),)

except* 語句會有選擇地匹配異常組中、符合的所有異常,並由下一個 except* 語句來嘗試匹配剩下的部分。

異常組的屬性與方法

編輯

message

編輯

對應創建異常組時,使用的參數 msg

對應創建異常組時,使用的參數 excs。它包含參數中所有異常組成的元組。

該屬性為只讀屬性。

subgroup(condition)

編輯

返回一個當前異常組中、匹配異常類型 condition 的異常組成的子組。如果沒有匹配的異常,則返回 None

子組將和原異常組有相同的 exception 信息。

split(condition)

編輯

返回一個有兩個元素的元組。元組第一個元素即為 self.subgroup(condition) ,而第二個元素為剩餘的異常組成的子組。

子組將和原異常組有相同的 exception 信息。

derive(excs)

編輯

subgroup 方法和 split 方法調用的、創建子異常組的方法。

如果要自定義異常組的子類,則應當重寫該方法。比如:

class MyGroup(ExceptionGroup):
    def derive(self, excs):
        return MyGroup(self,message, excs)

自定義子類的提示

編輯

要注意,BaseExceptionGroup 定義了 __new__ 方法。因此,在創建子類實例時,如果要更改構造子類實例的參數名稱或順序,或給子類添加屬性,應當重寫 __new__ 方法,而非重寫 __init__ 方法。比如:

class MyGroup(ExceptionGroup):
    def __new__(cls, errors, exit_code):
        self = super().__new__(Errors, str(exit_code), errors)
        self.exit_code = exit_code
        return self
    def derive(self, excs):
        return MyGroup(excs, self.exit_code)