關於Python異常處理,看這一篇就夠了

歡迎大家關注我的公衆號「HackDev」,會定期分享一些Python相關知識

當我們用Python編程的時候,常常會出現很多錯誤,大多數是語法錯誤,當然也有一些語義錯誤。例如,我就經常忘記在if語句後面加冒號,然後運行的時候就會報錯如下所示。

>>> if 5 % 2 == 1
  File "<stdin>", line 1
    if 5 % 2 == 1
                ^
SyntaxError: invalid syntax

如果我們使用Pycharm等IDE來寫代碼的時候,這種低級的語法錯誤會被IDE用紅色波浪線標註出來,會比較方便我們解決這些錯誤。

但是,除了這些錯誤,我們寫Python代碼的時候也很可能包含一些邏輯上的錯誤,例如TypeError就是我們經常遇到的一種錯誤,它表示數據類型不匹配。

>>> "This year is " + 2020 + "."
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "int") to str

例如上述代碼從字面上理解是字符串的拼接,但是由於2020它是int整型,而不是一個字符串,不能應用於字符串的拼接。所以產生了TypeError異常。這種在Python代碼執行過程中發生的非語法錯誤被稱爲異常。接下來,本文將介紹如何優雅地在Python中處理異常以及拋出異常。

 

異常處理的基本形式

處理異常的標準方法就是使用try...except語句。這一點其實比較類似於Java中的try...catch語句,事實上,大部分語言都有類似的捕捉異常的方法。

通常來說,可能產生異常的代碼應該被try語句囊括進去,如果報異常的就會立即停止try語句中的剩餘代碼,並執行except語句中的代碼。

我們可以看一個簡單示例

>>> # Declare a function that can handle ZeroDivisionError
>>> def divide_twelve(number):
...     try:
...         print(f"Result: {12/number}")
...     except ZeroDivisionError:
...         print("You can't divide 12 by zero.")
...
>>> # Use the function
>>> divide_twelve(6)
Result: 2.0
>>> divide_twelve(0)
You can't divide 12 by zero.

我們都知道0不能做分母,因此在上述代碼中,當0爲分母時就產生了一個異常。於是就執行except語句中的代碼了。

爲什麼要處理異常?

爲什麼要處理異常?因爲未經處理的異常會直接中斷Python程序的運行,舉個例子,假如我們部署一個Python寫的網站,其中一個用戶的操作觸發了某個Bug,導致程序產生了異常,Web服務就會直接被終止,所有用戶都不能訪問使用了,這顯然會造成巨大損失。

>>> # Define a function without handling
>>> def division_no_handle(x):
...     print(f"Result: {20/x}")
...     print("division_no_handle completes running")
...
>>> # Define a function with handling
>>> def division_handle(x):
...     try:
...         print(f"Result: {20/x}")
...     except ZeroDivisionError:
...         print("You can't divide a number with zero.")
...     print("division_handle completes running")
...
>>> # Call the functions
>>> division_handle(0)
You can't divide a number with zero.
division_handle completes running
>>> division_no_handle(0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in division_no_handle
ZeroDivisionError: division by zero

通過上面的代碼,我們也可以發現,當我們調用division_handle(0)時,由於異常得到了處理,程序還能繼續運行,但是,當我們調用沒有處理異常的division_no_handle(0)時,程序就直接中斷了。

變量分配

我們可以將異常賦值給一個變量,從而進一步瞭解該異常的相關信息,方便對該異常進行處理。例如在下面代碼中,我們可以將處理後的TypeError異常賦值給變量e,並將其打印出來。

>>> # A function that show error's detail
>>> def concat_messages(x, y):
...     try:
...         print(f"{x + y}")
...     except TypeError as e:
...         print(f"Argument Error: {e}")
...
>>> # Call the function
>>> concat_messages("Hello, ", 2020)
Argument Error: can only concatenate str (not "int") to str

當然,我們也可以一次捕捉多個異常,我們將可能的異常包裝在一個元組中,如下面的代碼片段中所示。當我們調用函數時,我們分別觸發ValueError和ZeroDivisionError。可以看到兩種異常都被捕獲到了

>>> # A function that handles multiple exceptions
>>> def divide_six(number):
...     try:
...         formatted_number = int(number)
...         result = 6/formatted_number
...     except (ValueError, ZeroDivisionError) as e:
...         print(f"Error {type(e)}: {e}")
...
>>> # Use the function
>>> divide_six("six")
Error <class 'ValueError'>: invalid literal for int() with base 10: 'six'
>>> divide_six(0)
Error <class 'ZeroDivisionError'>: division by zero

異常處理語句

1,多異常語句

事實上,我們可以通過多個except子句來處理多個異常,每個子句都處理一些特定的異常。如下所示。

>>> # A function that has multiple except clauses
>>> def divide_six(number):
...     try:
...         formatted_number = int(number)
...         result = 6/formatted_number
...     except ValueError:
...         print("This is a ValueError")
...     except ZeroDivisionError:
...         print("This is a ZeroDivisionError")
...
>>> # Use the function
>>> divide_six("six")
This is a ValueError
>>> divide_six(0)
This is a ZeroDivisionError

2,else語句

我們也可以在try ... except代碼塊中使用else子句。不過需要注意的是,else子句需要出現在except子句之後。只有當try子句完成而沒有引發任何異常時,else子句中的代碼纔會運行。如下例所示。

>>> # A function that has an else clause
>>> def divide_eight(number):
...     try:
...         result = 8/number
...     except:
...         print("divide_eight has an error")
...     else:
...         print(f"Result: {result}")
...
>>> # Use the function
>>> divide_eight(0)
divide_eight has an error
>>> divide_eight(4)
Result: 2.0

3,finally語句

finally子句放在塊的最末尾,將在整個try…except塊完成之後運行。只需要記住,無論是否產生異常,finally子句都會被執行就可以了,寫法和else語句是一樣的。

拋出異常

前面我們學習了使用try ... except塊來處理Python中異常,接下來我們來看看拋出異常(產生)異常的基本形式。

>>> # Raise exceptions
>>> raise Exception
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception
>>> raise NameError
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError
>>> raise ValueError()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError

如上所示,Python中使用raise關鍵字(Java中是throw關鍵字)後面跟上異常類(例如Exception,NameError)的方式來拋出異常。我們還可以使用異常類構造函數來創建實例,例如ValueError()。這兩種用法沒有區別,前者只是後者使用構造函數的語法糖。

1,自定義異常信息

我們還可以提供有關我們提出的異常的其他信息。最簡單的方法是使用異常類構造函數,幷包含適用的錯誤消息來創建實例。如下所示:

>>> raise ValueError("You can't divide something with zero.")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: You can't divide something with zero.
>>> raise NameError("It's silly to make this mistake.")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: It's silly to make this mistake.

2,再拋出異常
如圖所示,所有異常都會在被捕獲時進行處理。但是,有可能我們可以重新拋出異常並將異常傳遞給外部範圍,以查看是否可以處理該異常。

當我們編寫涉及嵌套結構的複雜代碼時(例如,一個函數調用另一個函數,該函數可能會調用另一個函數),此功能會更加有用。通過再拋出異常,我們可以決定在哪裏處理特定異常。當然,根據具體情況確定處理特定異常的確切位置。我們先來看一些代碼:

>>> # Define two functions with one calling the other
>>> def cast_number(number_text, to_raise):
...     try:
...         int(number_text)
...     except:
...         print("Failed to cast")
...         if to_raise:
...             print("Re-raise the exception")
...             raise
...
>>> def run_cast_number(number_text, to_raise):
...     try:
...         cast_number(number_text, to_raise)
...     except:
...         print("Handled in run_cast_number")
...
>>> # Use the functions
>>> run_cast_number("six", False)
Failed to cast
>>> run_cast_number("six", True)
Failed to cast
Re-raise the exception
Handled in run_cast_number

在上面的代碼中,我們有兩個函數,其中run_cast_number調用另一個函數cast_number。我們使用字符串兩次調用該函數,這兩個函數都會導致異常,因此將顯示消息“ Fasted to cast”,因爲該異常是在cast_number函數中處理的。但是,第二次調用函數,我們要求cast_number函數重新引發異常,以便except子句在run_cast_number函數中運行。

3,用戶自定義異常

在許多情況下,我們可以使用內置的異常來幫助我們在項目中引發和處理異常。但是,Python使我們可以靈活地創建自己的自定義異常類。也就是說,我們需要將一個類聲明爲內置Exception類的子類。

>>> # Define a custom exception class
>>> class FileExtensionError(Exception):
... def __init__(self, filename, desired_ext):
... self.filename = filename
... self.desired_ext = desired_ext
... def __str__(self):
... return f"File {self.filename} should have the extension: {self.desired_ext}."
...
>>> # Raise custom exceptions
>>> raise FileExtensionError
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() missing 2 required positional arguments: 'filename' and 'desired_ext'
>>> raise FileExtensionError("test.xls", "csv")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
__main__.FileExtensionError: File test.xls should have the extension: csv.

如上所示,我們創建了一個名爲FileExtensionError的自定義異常類。當我們拋出這個異常時,僅使用類名是行不通的。相反,我們應該通過爲構造方法設置兩個位置參數來實例化此異常。通過實現__str__方法,我們可以看到自定義異常消息。換句話說,異常消息是通過調用str()函數生成的。

 

4, 何時應該拋出異常

一般來說,當代碼可能會在某些情況下會報錯無法執行時,就應該拋出異常。

 

最後,再次安利一下我的公衆號「HackDev」,會定期分享一些Python相關知識。

參考資料:https://medium.com/better-programming/how-to-handle-and-raise-exceptions-in-python-12-things-to-know-4dfef7f02e4

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章