Python 中的異常處理

錯誤和異常

目前在 Python 中(至少)有兩種可區分的錯誤:語法錯誤異常html

語法錯誤

語法錯誤又稱解析錯誤,多是在學習 Python 時最容易遇到的錯誤:python

>>> while True print('Hello world')
  File "<stdin>", line 1
    while True print('Hello world')
                   ^
SyntaxError: invalid syntax
複製代碼

異常

在執行時檢測到的錯誤被稱爲異常,大多數異常並不會被程序自動處理,此時會顯示以下所示的錯誤信息:程序員

>>> 10 * (1/0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't convert 'int' object to str implicitly
複製代碼

錯誤信息的最後一行告訴咱們程序遇到了什麼類型的錯誤。異常有不一樣的類型,而其類型名稱將會做爲錯誤信息的一部分中打印出來。這一行的剩下的部分根據異常類型及其緣由提供詳細信息。編程

錯誤信息的前一部分以堆棧回溯的形式顯示發生異常時的上下文。一般它包含列出源代碼行的堆棧回溯;可是它不會顯示從標準輸入中讀取的行。api

做爲異常類型打印的字符串是發生的內置異常的名稱。對於全部內置異常都是如此,但對於用戶定義的異常則不必定如此(雖然這是一個有用的規範)。標準的異常類型是內置的標識符(而不是保留關鍵字)。bash

內置異常

篇幅問題,請參考:Python 中的內置異常網絡

異常處理

異常處理工做由「捕獲」和「拋出」兩部分組成。「捕獲」指的是使用 try ... except 包裹特定語句,穩當的完成錯誤流程處理。而恰當的使用 raise 主動「拋出」異常,更是優雅代碼裏必不可少的組成部分。函數

捕獲

try 語句的工做原理
  1. 首先,執行 try 子句(tryexcept 關鍵字之間的(多行)語句)。
  2. 若是沒有異常發生,則跳過 except 子句並完成 try 語句的執行。
  3. 若是在執行 try 子句時發生了異常,則跳過該子句中剩下的部分。而後,若是異常的類型和 except 關鍵字後面的異常匹配,則執行 except 子句 ,而後繼續執行 try 語句以後的代碼。
  4. 若是發生的異常和 except 子句中指定的異常不匹配,則將其傳遞到外部的 try 語句中;若是沒有找處處理程序,則它是一個未處理異常,執行將中止並顯示錯誤的消息。

一個 try 語句可能有多個 except 子句,以指定不一樣異常的處理程序,但最多會執行一個處理程序。處理程序只處理相應的 try 子句中發生的異常,而不處理同一 try 語句內其餘處理程序中的異常。一個 except 子句能夠將多個異常命名爲帶括號的元組,例如:學習

... except (RuntimeError, TypeError, NameError):
...     pass
複製代碼

若是發生的異常和 except 子句中的類是同一個類或者是它的基類,則異常和 except 子句中的類是兼容的(但反過來則不成立)。例如,下面的代碼將依次打印 B, C, Dui

class B(Exception):
    pass

class C(B):
    pass

class D(C):
    pass

for cls in [B, C, D]:
    try:
        raise cls()
    except D:
        print("D")
    except C:
        print("C")
    except B:
        print("B")
複製代碼

請注意若是 except 子句被顛倒(把 except B 放到第一個),它將打印 B,B,B --- 即第一個匹配的 except 子句被觸發。

最後的 except 子句能夠省略異常名,以用做通配符。但請謹慎使用,由於以這種方式很容易掩蓋真正的編程錯誤!它還可用於打印錯誤消息,而後從新引起異常(一樣容許調用者處理異常)。

try ... except 語句有一個可選的 else 子句,在使用時必須放在全部的 except 子句後面。對於在 try 子句不引起異常時必須執行的代碼來講頗有用。

使用 else 子句比向 try 子句添加額外的代碼要好,由於它避免了意外捕獲由 try ... except 語句保護的代碼未引起的異常。

異常處理程序不只處理 try 子句中遇到的異常,還處理 try 子句中調用(即便是間接地)的函數內部發生的異常。

異常參數

發生異常時,它可能具備關聯值,也稱爲異常參數。參數的存在和類型取決於異常類型

except 子句能夠在異常名稱後面指定一個變量。這個變量和一個異常實例綁定,它的參數存儲在 instance.args 中。爲了方便起見,異常實例定義了 __str__(),所以能夠直接打印參數而無需引用 .args。也能夠在拋出以前首先實例化異常,並根據須要向其添加任何屬性。

>>> try:
...     raise Exception('spam', 'eggs')
... except Exception as inst:
...     print(type(inst))    # the exception instance
...     print(inst.args)     # arguments stored in .args
...     print(inst)          # __str__ allows args to be printed directly,
...                          # but may be overridden in exception subclasses
...     x, y = inst.args     # unpack args
...     print('x =', x)
...     print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs
複製代碼

若是異常有參數,則它們將做爲未處理異常的消息的最後一部分(詳細信息)打印。

拋出

raise 語句容許程序員強制發生指定的異常。例如:

>>> raise NameError('HiThere')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: HiThere
複製代碼

raise 惟一的參數就是要拋出的異常。這個參數必須是一個異常實例或者是一個異常(派生自 Exception 的類)。若是傳遞的是一個異常類,它將經過調用沒有參數的構造函數來隱式實例化:

raise ValueError  # raise ValueError() 的簡寫
複製代碼

若是你須要肯定是否引起了異常但不打算處理它,則可使用更簡單的 raise 語句形式從新引起異常:

>>> try:
...     raise NameError('HiThere')
... except NameError:
...     print('An exception flew by!')
...     raise
...
An exception flew by!
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
NameError: HiThere
複製代碼

用戶自定義異常

程序能夠經過建立新的異常類來命名它們本身的異常。異常一般應該直接或間接地從 Exception 類派生。

能夠定義異常類,它能夠執行任何其餘類能夠執行的任何操做,但一般保持簡單,一般只提供許多屬性,這些屬性容許處理程序爲異常提取有關錯誤的信息。在建立可能引起多個不一樣錯誤的模塊時,一般的作法是爲該模塊定義的異常建立基類,併爲不一樣錯誤條件建立特定異常類的子類。

大多數異常都定義爲名稱以 Error 結尾,相似於標準異常的命名。

許多標準模塊定義了它們本身的異常,以報告它們定義的函數中可能出現的錯誤。

定義清理操做

try 語句有另外一個可選子句,用於定義必須在全部狀況下執行的清理操做。

finally 子句總會在離開 try 語句前被執行,不管是否發生了異常。當在 try 子句中發生了異常且還沒有被 except 子句處理(或者它發生在 exceptelse 子句中)時,它將在 finally 子句執行後被從新拋出。當 try 語句的任何其餘子句經過 break, continue, return 語句離開時,finally 也會在「離開以前」被執行。

在實際應用程序中,finally 子句對於**釋放外部資源(例如文件或者網絡鏈接)**很是有用,不管是否成功使用資源。

進行異常處理時的小技巧

傳遞異常

有時咱們會在捕捉到一個異常後從新引起它(傳遞異常),實現起來很簡單,使用不帶參數的 raise 語句便可,例如:

def f1():
    print(1/0)

def f2():
    try:
        f1()
    except Exception as e:
        print('something worng')
        raise

f2()
複製代碼
# 運行結果
something worng
Traceback (most recent call last):
  File "/Users/ryoma/Desktop/project/learn/learn_python/python_exception.py", line 11, in <module>
    f2()
  File "/Users/ryoma/Desktop/project/learn/learn_python/python_exception.py", line 6, in f2
    f1()
  File "/Users/ryoma/Desktop/project/learn/learn_python/python_exception.py", line 2, in f1
    print(1/0)
ZeroDivisionError: division by zero
複製代碼

使用內置的語法規範代替 try/except

Python 自己提供了不少語法範式簡化了異常處理,例如:

  1. for 語句利用 Stoplteration 異常來結束循環的
  2. with 語句在打開文件後會在操做結束後(不管是否正常結束)會自動關閉文件句柄
  3. 使用 getattr() 函數獲取對象中的不肯定屬性

以上這些都是 Python 自身封裝好的語法範式,在處理這些事件的時候應避免使用 try/except/finally 的思惟來處理。

異常處理的三個好習慣

只作精確的異常捕獲

Python 中使用異常捕獲的目的並非使本身寫的代碼不出現任何異常,而是在可能因外部力量而出錯的部分進行預防,例如對用戶輸入部分進行異常捕獲。

Python 中使用異常捕獲時應捕獲儘量精確的異常類型,而不是模糊的 Exception,由於模糊的捕獲 Exception 有時會致使本該被顯示的有用的錯誤信息被自定義的錯誤信息「吃」掉。

另外,使本身寫的代碼不出現任何異常的最好方法是規範的代碼書寫習慣。

別讓異常破壞代碼抽象分層的一致性

不少場景下咱們會對異常類進行包裝,方便在產生已知異常時自定義錯誤信息,這樣作能大大提升後續的編碼效率,但在使用時若是沒有作好分層處理很容易擊穿代碼的抽象分層邏輯,具體案例請參考 Python 工匠: 異常處理的三個好習慣

爲了不由於使用錯誤的異常處理方式致使代碼的抽象分層邏輯被打破:

  1. 讓模塊只調用與當前抽象層級一致的異常類,既不能高於當前抽象層級,也不能低於當前抽象層級
  2. 在須要跨層級調用異常類時應經過異常包裝與轉換的方法進行,而不是直接跨層級調用異常類

異常處理不該該喧賓奪主

當非異常處理邏輯代碼中存在大量異常處理操做時,很容易出現因異常處理的邏輯代碼太多而擾亂核心的邏輯代碼。

# 代碼來自:Python 工匠:異常處理的三個好習慣
def upload_avatar(request):
    """用戶上傳新頭像"""
    try:
        avatar_file = request.FILES['avatar']
    except KeyError:
        raise error_codes.AVATAR_FILE_NOT_PROVIDED

    try:
       resized_avatar_file = resize_avatar(avatar_file)
    except FileTooLargeError as e:
        raise error_codes.AVATAR_FILE_TOO_LARGE
    except ResizeAvatarError as e:
        raise error_codes.AVATAR_FILE_INVALID

    try:
        request.user.avatar = resized_avatar_file
        request.user.save()
    except Exception:
        raise error_codes.INTERNAL_SERVER_ERROR
    return HttpResponse({})
複製代碼

此時咱們可使用 Python 中的 **上下文管理器(context manager)**配合 with 語句簡化異常處理過程。

# 代碼來自:Python 工匠:異常處理的三個好習慣
class raise_api_error:
    """captures specified exception and raise ApiErrorCode instead :raises: AttributeError if code_name is not valid """
    def __init__(self, captures, code_name):
        self.captures = captures
        self.code = getattr(error_codes, code_name)

    def __enter__(self):
        # 剛方法將在進入上下文時調用
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        # 該方法將在退出上下文時調用
        # exc_type, exc_val, exc_tb 分別表示該上下文內拋出的
        # 異常類型、異常值、錯誤棧
        if exc_type is None:
            return False

        if exc_type == self.captures:
            raise self.code from exc_val
        return False
複製代碼

在上面的代碼裏,定義了一個名爲 raise_api_error 的上下文管理器,它在進入上下文時什麼也不作。可是在退出上下文時,會判斷當前上下文中是否拋出了類型爲 self.captures 的異常,若是有,就用 APIErrorCode 異常類替代它。

使用該上下文管理器後,上面臃腫的 upload_avatar 函數變得更清晰簡潔:

# 代碼來自:Python 工匠:異常處理的三個好習慣
def upload_avatar(request):
    """用戶上傳新頭像"""
    with raise_api_error(KeyError, 'AVATAR_FILE_NOT_PROVIDED'):
        avatar_file = request.FILES['avatar']

    with raise_api_error(ResizeAvatarError, 'AVATAR_FILE_INVALID'),\
            raise_api_error(FileTooLargeError, 'AVATAR_FILE_TOO_LARGE'):
        resized_avatar_file = resize_avatar(avatar_file)

    with raise_api_error(Exception, 'INTERNAL_SERVER_ERROR'):
        request.user.avatar = resized_avatar_file
        request.user.save()
    return HttpResponse({})
複製代碼

參考

感謝參考文章的做者(譯者)

Python 3.7.4 中文文檔-錯誤和異常

Python 工匠: 異常處理的三個好習慣

地球的外星人君:一文掌握 Python 異常處理的全部知識點

相關文章
相關標籤/搜索