不管是智商157或更高的程式設計師撰寫程式碼時,都無法保證系統不會出現錯誤,因此,我們只能在程式出現非預期的錯誤時,進行善後處理,顯示錯誤原因或修正錯誤的建議,這種機制稱之為【例外處理】(Exception),也稱為try/catch機制,即捕捉(catch)錯誤後,作出適當回應。本文將介紹以下內容:
許多運行多年的應用系統也會發現缺乏例外處理的機制或處理不當的案例,輕則造成系統不穩定,嚴重的可能暴露系統資訊,提供駭客可乘之機,例如輸入不完整的網址,竟然出現ASP.NET範例網頁:
圖一. 路由設定不當,出現非預期的錯誤
輸入錯誤網址,竟然出現原生的錯誤訊息:
圖二. 未使用try/catch機制,網頁出現原始的錯誤訊息
Python例外處理分為3塊:
我們直接以實際範例說明try/except/finally用法。
範例1. 簡單測試,程式名稱為try_catch1.py。
try:
result = 1 / 0 #除以0,發生錯誤
except Exception as e:
print(e)
finally:
print('done.')
division by zero
done.
try:
result = 1 / 0 #除以0,發生錯誤
except Exception as e:
print(f'錯誤:(10001) {e}') # 修飾
錯誤:(10001) division by zero
範例2. 範例1並沒有顯示錯誤行號,要列印完整錯誤訊息,可使用traceback.print_exc(),程式名稱為try_catch2.py。
import traceback
try:
result = 1 / 0 #除以0,發生錯誤
except Exception as e:
traceback.print_exc()
Traceback (most recent call last):
File "try_catch2.py", line 4, in <module>
result = 1 / 0 #除以0,發生錯誤
ZeroDivisionError: division by zero
print(f'錯誤:(10001) {e}')
print(f'{"-"*10} 詳細內容 {"-"*10}')
traceback.print_exc()
print(f'{"-"*30}')
錯誤:(10001) division by zero
---------- 詳細內容 ----------
Traceback (most recent call last):
File "try_catch2.py", line 4, in <module>
result = 1 / 0 #除以0,發生錯誤
ZeroDivisionError: division by zero
------------------------------
範例3. 通常使用者不喜歡看到太多的錯誤訊息以及一堆英文術語,因此可以使用logging.exception指令把詳細錯誤內容寫入工作日誌,完整程式請參閱try_catch_log.py。
try:
result = 1 / 0 #除以0,發生錯誤
except Exception as e:
print(f'錯誤:(10001) {e}')
logging.exception(f'錯誤:(10001) {e}\n{"-"*10} 詳細內容 {"-"*10}')
04:29:52 ERROR:錯誤:(10001) division by zero
---------- 詳細內容 ----------
Traceback (most recent call last):
File "try_catch_log.py", line 17, in <module>
result = 1 / 0 #除以0,發生錯誤
ZeroDivisionError: division by zero
raise指令可主動引發例外,有時候並非程式錯誤,而是商業邏輯錯誤,可以使用raise指令主動引發例外,交由外層的程式try/except處理,或者希望改寫預設的錯誤訊息,可以在捕捉到錯誤後,在except段落內,執行raise指令重新定義新的錯誤訊息。
範例4. raise指令測試,程式名稱為raise1.py。
x = -1
try:
if x <= 0:
raise Exception('訂購數量不得小於或等於0 !!')
except Exception as e:
print(f'錯誤:(10001) {e}')
錯誤:(10001) 訂購數量不得小於或等於0 !!
範例5. 改寫預設的錯誤訊息,程式名稱為raise2.py。
import sys, os
def test_raise():
try:
result = 1 / 0
except Exception as e:
file_name = os.path.basename(__file__) # 錯誤檔名
line_no = sys.exc_info()[-1].tb_lineno # 錯誤發生行號
raise Exception(f'{file_name} 第 {line_no} 行錯誤:不可以除以0 !!')
if __name__ == '__main__':
# 測試
try:
test_raise()
except Exception as e:
print(f'錯誤:(10001) {e}')
錯誤:(10001) raise2.py 第 5 行錯誤:不可以除以0 !!
try/catch機制使用很容易,但一定要注意使用時機:
針對以上各項一一舉例說明。
範例6. 在公用函數庫使用try/except機制的副作用,程式名稱為try_except_misuse.py、common_functions.py。
def divide(a, b):
try:
result = a / b
return result
except Exception as e:
print(f'錯誤:(10001) {e}')
return 0
from common_functions import divide
if __name__ == '__main__':
# 測試
try:
c = divide(1, 0) # 發生錯誤,但divide
print(c+5)
except Exception as e:
print(f'錯誤:(10001) {e}')
錯誤:(10001) division by zero
5
...
except Exception as e:
print(f'錯誤:(10001) {e}')
raise Exception(f'{file_name} 第 {line_no} 行錯誤:不可以除以0 !!')
return 0
def divide(a, b):
result = a / b
return result
範例7. 以下程式碼可能發生多種錯誤,包括【無此檔案】(FileNotFoundError)、【除以0】(ZeroDivisionError)或【資料型別錯誤】(ValueError),大部份的文章都建議以下寫法,程式名稱為multiple_exception_type.py,區分錯誤原因。
try:
f = open('./data.txt', encoding='utf8')
content = f.read()
a, b = int(content.split()[0]), int(content.split()[1])
result = a / b
except FileNotFoundError as e: # 無 data.txt 檔案
print(f'錯誤:(10001) {e}')
except ValueError as e: # 資料型別轉換錯誤,例如 'a' 要轉換為數值
print(f'錯誤:(10002) {e}')
except ZeroDivisionError as e: # 除以0,例如 1/0
print(f'錯誤:(10003) {e}')
except Exception as e: # 其他錯誤
print(f'錯誤:(10004) {e}')
筆者作法是將程式碼切成多個try/except段落,會比較容易識別發生錯誤的地方,且程式段落比較明確,程式名稱為multiple_exception_type2.py。
import sys
# 開啟檔案
try:
f = open('./data.txt', encoding='utf8')
content = f.read()
except Exception as e:
print(f'錯誤:(10001) {e}')
sys.exit(1)
# 計算
try:
a, b = int(content.split()[0]), int(content.split()[1])
result = a / b
except Exception as e:
print(f'錯誤:(10002) {e}')
sys.exit(2)
sys.exit(0)
範例8. 以Flask為例,在事件處理函數內編程,程式名稱為multiple_exception_type_flask.py。
import sys
from flask import Flask, request
app = Flask(__name__)
@app.route("/", methods=['GET'])
def test1():
# 開啟檔案
try:
f = open('./data.txt', encoding='utf8')
content = f.read()
except Exception as e:
return f'錯誤:(10001) {e}'
# 計算
try:
a, b = int(content.split()[0]), int(content.split()[1])
result = a / b
return result
except Exception as e:
return f'錯誤:(10002) {e}'
if __name__ == '__main__':
app.run()
錯誤:(10002) division by zero
assert指令可用在不太可能發生錯誤的地方作進一步檢查,一旦斷言(assert)不成立(False),assert會主動觸發例外(raise)。與數學定理類似,任何定理都有假設(Assumption),應用前我們必須先審視假設是否成立。
範例9. assert簡單測試,程式名稱為assert1.py。
def divide(a, b):
assert (b != 0), f"{b}不可為0 !!"
result = a / b
return result
if __name__ == '__main__':
divide(1, 0)
Traceback (most recent call last):
File "F:\0_python\00_MY\0_ITHome\src\9\assert1.py", line 7, in <module>
divide(1, 0)
File "F:\0_python\00_MY\0_ITHome\src\9\assert1.py", line 2, in divide
assert (b != 0), f"{b}不可為0 !!"
AssertionError: 0不可為0 !!
更多的assert用法可參閱【Python's assert: Debug and Test Your Code Like a Pro】。
例外處理可避免應用程式直接當掉或顯示原生的錯誤訊息,是系統開發不可或缺的技能,缺乏例外處理或過度使用都會造成系統隱憂,輕則造成使用者不良觀感,重則會造成系統不穩定、曝露資安漏洞,釀成大禍,不可不慎。
本系列的程式碼會統一放在GitHub,本篇的程式放在src/9資料夾,歡迎讀者下載測試,如有錯誤或疏漏,請不吝指正。