異常處理:如何提高程序穩定性?-day6

異常處理:如何提高程序穩定性?

寫在前面

你好,我是禪墨!

今天,我想和你聊聊 Python 的異常處理。和其他語言一樣,異常處理是 Python 中一種很常見,並且很重要的機制與代碼規範。

在實際工作中,很多這樣的情況:一位工程師提交了代碼,不過代碼某處忘記了異常處理。碰巧這種異常發生的頻率不低,所以在代碼 push 到線上後沒多久,就會收到緊急通知——服務器崩潰了。如果事情嚴重,對用戶的影響也很大,這位工程師還得去專門的會議上做自我檢討,可以說是很慘了。這類事件層出不窮,也告訴我們,正確理解和處理程序中的異常尤爲關鍵。

錯誤與異常

首先要了解,Python 中的錯誤和異常是什麼?兩者之間又有什麼聯繫和區別呢?

通常來說,程序中的錯誤至少包括兩種,一種是語法錯誤,另一種則是異常。

所謂語法錯誤,你應該很清楚,也就是你寫的代碼不符合編程規範,無法被識別與執行,比如下面這個例子:


if name is not None
    print(name)

If 語句漏掉了冒號,不符合 Python 的語法規範,所以程序就會報錯invalid syntax。

而異常則是指程序的語法正確,也可以被執行,但在執行過程中遇到了錯誤,拋出了異常,比如下面的 3 個例子:


10 / 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: integer division or modulo by zero

order * 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'order' is not defined

1 + [1, 2]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'list'

它們語法完全正確,但顯然,我們不能做除法時讓分母爲 0;也不能使用未定義的變量做運算;而讓一個整型和一個列表相加也是不可取的。於是,當程序運行到這些地方時,就拋出了異常,並且終止運行。例子中的ZeroDivisionError NameError和TypeError,就是三種常見的異常類型。當然,Python 中還有很多其他異常類型,比如KeyError是指字典中的鍵找不到;FileNotFoundError是指發送了讀取文件的請求,但相應的文件不存在等等,我在此不一一贅述。可自行參考文檔

處理異常

剛剛講到,如果執行到程序中某處拋出了異常,程序就會被終止並退出。你可能會問,那有沒有什麼辦法可以不終止程序,讓其照樣運行下去呢?答案當然是肯定的,這也就是我們所說的異常處理,通常使用 try 和 except 來解決,比如:


try:
    s = input('please enter two numbers separated by comma: ')
    num1 = int(s.split(',')[0].strip())
    num2 = int(s.split(',')[1].strip())
    ... 
except ValueError as err:
    print('Value Error: {}'.format(err))

print('continue')
...

這裏默認用戶輸入以逗號相隔的兩個整形數字,將其提取後,做後續的操作(注意 input 函數會將輸入轉換爲字符串類型)。如果我們輸入a,b,程序便會拋出異常invalid literal for int() with base 10: ‘a’,然後跳出 try 這個 block。

由於程序拋出的異常類型是 ValueError,和 except block 所 catch 的異常類型相匹配,所以 except block 便會被執行,最終輸出Value Error: invalid literal for int() with base 10: ‘a’,並打印出continue。

please enter two numbers separated by comma: a,b
Value Error: invalid literal for int() with base 10: ‘a’
continue

我們知道,except block 只接受與它相匹配的異常類型並執行,如果程序拋出的異常並不匹配,那麼程序照樣會終止並退出。

所以,還是剛剛這個例子,如果我們只輸入1,程序拋出的異常就是IndexError: list index out of range,與 ValueError 不匹配,那麼 except block 就不會被執行,程序便會終止並退出(continue 不會被打印)。

please enter two numbers separated by comma: 1
IndexError Traceback (most recent call last)
IndexError: list index out of range

不過,很顯然,這樣強調一種類型的寫法有很大的侷限性。那麼,該怎麼解決這個問題呢?

其中一種解決方案,是在 except block 中加入多種異常的類型,比如下面這樣的寫法:

#第一種
try:
    s = input('please enter two numbers separated by comma: ')
    num1 = int(s.split(',')[0].strip())
    num2 = int(s.split(',')[1].strip())
    ...
except (ValueError, IndexError) as err:
    print('Error: {}'.format(err))
    
print('continue')
...

#第二種

try:
    s = input('please enter two numbers separated by comma: ')
    num1 = int(s.split(',')[0].strip())
    num2 = int(s.split(',')[1].strip())
    ...
except ValueError as err:
    print('Value Error: {}'.format(err))
except IndexError as err:
    print('Index Error: {}'.format(err))

print('continue')
...

這樣,每次程序執行時,except block 中只要有一個 exception 類型與實際匹配即可。

不過,很多時候,我們很難保證程序覆蓋所有的異常類型,所以,更通常的做法,是在最後一個 except block,聲明其處理的異常類型是 Exception。Exception 是其他所有非系統異常的基類,能夠匹配任意非系統異常。那麼這段代碼就可以寫成下面這樣:


try:
    s = input('please enter two numbers separated by comma: ')
    num1 = int(s.split(',')[0].strip())
    num2 = int(s.split(',')[1].strip())
    ...
except ValueError as err:
    print('Value Error: {}'.format(err))
except IndexError as err:
    print('Index Error: {}'.format(err))
except Exception as err:
    print('Other error: {}'.format(err))

print('continue')
...

或者,你也可以在 except 後面省略異常類型,這表示與任意異常相匹配(包括系統異常等):


try:
    s = input('please enter two numbers separated by comma: ')
    num1 = int(s.split(',')[0].strip())
    num2 = int(s.split(',')[1].strip())
    ...
except ValueError as err:
    print('Value Error: {}'.format(err))
except IndexError as err:
    print('Index Error: {}'.format(err))
except:
    print('Other error')

print('continue')
...

需要注意,當程序中存在多個 except block 時,最多隻有一個 except block 會被執行。換句話說,如果多個 except 聲明的異常類型都與實際相匹配,那麼只有最前面的 except block 會被執行,其他則被忽略。

異常處理中,還有一個很常見的用法是 finally,經常和 try、except 放在一起來用。無論發生什麼情況,finally block 中的語句都會被執行,哪怕前面的 try 和 excep block 中使用了 return 語句。

一個常見的應用場景,便是文件的讀取:


import sys
try:
    f = open('file.txt', 'r')
    .... # some data processing
except OSError as err:
    print('OS error: {}'.format(err))
except:
    print('Unexpected error:', sys.exc_info()[0])
finally:
    f.close()

這段代碼中,try block 嘗試讀取 file.txt 這個文件,並對其中的數據進行一系列的處理,到最後,無論是讀取成功還是讀取失敗,程序都會執行 finally 中的語句——關閉這個文件流,確保文件的完整性。因此,在 finally 中,我們通常會放一些無論如何都要執行的語句。

值得一提的是,對於文件的讀取,我們也常常使用 with open,你也許在前面的例子中已經看到過,with open 會在最後自動關閉文件,讓語句更加簡潔。用戶自定義異常

自定義異常

前面的例子裏充斥了很多 Python 內置的異常類型,你可能會問,我可以創建自己的異常類型嗎?

答案是肯定是,Python 當然允許我們這麼做。下面這個例子,我們創建了自定義的異常類型 MyInputError,定義並實現了初始化函數和 str 函數(直接 print 時調用):


class MyInputError(Exception):
    """Exception raised when there're errors in input"""
    def __init__(self, value): # 自定義異常類型的初始化
        self.value = value
    def __str__(self): # 自定義異常類型的string表達形式
        return ("{} is invalid input".format(repr(self.value)))
    
try:
    raise MyInputError(1) # 拋出MyInputError這個異常
except MyInputError as err:
    print('error: {}'.format(err))

如果你執行上述代碼塊並輸出,便會得到下面的結果:

error: 1 is invalid input

實際工作中,如果內置的異常類型無法滿足我們的需求,或者爲了讓異常更加詳細、可讀,想增加一些異常類型的其他功能,我們可以自定義所需異常類型。不過,大多數情況下,Python 內置的異常類型就足夠好了。

使用場景與注意點

學完了前面的基礎知識,接下來我們着重談一下,異常的使用場景與注意點。

通常來說,在程序中,如果我們不確定某段代碼能否成功執行,往往這個地方就需要使用異常處理。除了上述文件讀取的例子,我可以再舉一個例子來說明。

大型社交網站的後臺,需要針對用戶發送的請求返回相應記錄。用戶記錄往往儲存在 key-value 結構的數據庫中,每次有請求過來後,我們拿到用戶的 ID,並用 ID 查詢數據庫中此人的記錄,就能返回相應的結果。

而數據庫返回的原始數據,往往是 json string 的形式,這就需要我們首先對 json string 進行 decode(解碼),你可能很容易想到下面的方法:


import json
raw_data = queryDB(uid) # 根據用戶的id,返回相應的信息
data = json.loads(raw_data)

這樣的代碼是不是就足夠了呢?

要知道,在 json.loads() 函數中,輸入的字符串如果不符合其規範,那麼便無法解碼,就會拋出異常,因此加上異常處理十分必要。


try:
    data = json.loads(raw_data)
    ....
except JSONDecodeError as err:
    print('JSONDecodeError: {}'.format(err))

誠然,這樣的代碼並沒有 bug,但是讓人看了摸不着頭腦,也顯得很冗餘。如果你的代碼中充斥着這種寫法,無疑對閱讀、協作來說都是障礙。因此,對於 flow-control(流程控制)的代碼邏輯,我們一般不用異常處理。

字典這個例子,寫成下面這樣就很好。


if 'dob' in d:
    value = d['dob']
    ...

寫在後面

最近這幾篇的閱讀量上不去啊,啊啊啊啊

我就告訴自己,再堅持堅持!加油!

CSDN:禪墨雲
知乎: 禪墨雲
個人博客:禪墨雲

在這裏插入圖片描述

公衆號:興趣路人甲

這裏是引用

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