- 登入
- 註冊
Python 其實一直有一個相對弱的部分,就是廣大的生態系與眾多的套件中,因為寫法比較模糊,沒有很多靜態語言的風格,故在執行過程中,如果要知道問題出在哪,就將會是很大的問題。
而在常見的異常處理中,也很難針對錯誤的地方找到真正的錯誤,往往套件越複雜,或是導入的套件越多、層數越多,該功能能抓出準確問題的能力也就越困難。
故這篇文章會向大家介紹常見而且我個人覺得實用的做法,但在一開始,我希望先大概介紹一下(不會實作這件事)單元測試的邏輯與情境,以便讓各位在未來的環境中,對於程式碼 debug、異常處理、維護與修改中,有更深一層的理解,就讓我們開始吧!
(以下概念暫時不懂沒關係,但我還是希望讀者可以先至少知道這件事的存在,未來用到會至少有聽過?)
這邊稍微介紹一下單元測試的核心理念,通常我們在跑程式碼的時候,最擔心的就是程式碼出錯,而單元(Unit)這個概念更可以理解成一整包程式碼中的每個部件,小到變數的存取與呼叫,大到整個物件的呼叫與繼承(OOP),都是可以被測試的一環。
或許有人會問:「我一個簡單的邏輯,為何要寫兩份程式碼;2分鐘的任務,變成5分鐘的理由是啥呢?」
答案也很簡單,隨著程式碼的更迭、累積,後續維護的可能不是開發者,那系統越來越多功能時,如何保證程式碼的每個功能都能運作正常呢?
這時後平時所寫好的測試就會是一個非常適合迭代的行為,當然,測試分成很多種這邊不展開講,但可以基本理解的就是,測試的目標是為了維持程式過段時間仍然可以正常運作。即便加了新功能,只要一跑,就會知道過去的舊功能是否仍然可以執行,也更可以知道是否會有預期外的錯誤會發生。
好了,說了個大概後,就進入的今天的重點,錯誤一旦發生,程式該怎麼辦呢?
對於程式碼的錯誤處理,與其說是針對錯誤去做某些事,不如說是針對可以預期的幾種錯誤去做對應的解決方案,這類型問題在程式運算流程中。
只要資料來源、存取、處理、甚至於套件的使用與繼承,都不是自己可以完全控制的時候(有時候甚至連自己可以100%控制都得做),基本上都會需要做捕捉異常的事情。
這部分可以探討的很細,關於 Desgin pattern(設計原則)中有針對該議題提到的相關的細節,這邊不會特別展開說。
至於到底何時該做,何時不該做,大概的分水嶺可以分成:
def not_error_func(a ,b, c):
return a + b + c
例如上述函式,知道回傳是三個函數的計算,所以我們可以知道丟進去的參數勢必是 int 或是 float等可計算數值。
那這時候這種功能如果非常的硬性,可以做以下兩種作法:
A. 直接在創建函式時就確認參數是數值,故如果丟進去的參數不是可計算數值,則會報錯
def not_error_func(a=int, b=int, c=float):
return a + b + c
B. 在內部寫異常處理
def not_error_func(a, b, c):
try:
return a + b + c
except Exception as e:
print(e)
print(not_error_func(345, 34, "56"))
輸出結果:
unsupported operand type(s) for +: 'int' and 'str'
None
雖然用 Exception 捕捉到了錯誤,但這樣的捕捉其實是給人看的,程式很難針對其作法做出其他行為,這部分底下會一併解釋。
這部分的錯誤比較偏向於在 web 上使用別人的 API,或是呼叫別人的數據庫,因為你不能準確確定對方來源的資料格式是否會變,或是內容可以改版後型態會變,甚至變數名稱會變。
那自然就很難針對該情形去寫完整的異常處理。假設以下資料為API回傳的資料,我們希望可以將資料內部的 vol 相加:
test_data = {
"code": 0,
"status": 200,
"data": [
{
"symbol": "BTCUSDT",
"price": "30000",
"amount": "3",
"base": "USDT",
"asset": "BTC",
"vol": "32130",
"status": True
},
{
"symbol": "ETHUSDT",
"price": "2000",
"amount": "3",
"base": "USDT",
"asset": "BTC",
"vol": "32130",
"status": True
},
]
}
想當然,因為vol是str,所以相加肯定是會報錯的,故函式可以將相加的數據強制轉成數值:
def get_data(data):
sum_vol = []
for d in data['data']:
sum_vol.append(float(d['vol']))
return sum(sum_vol)
print(get_data(api_back))
但這樣會有幾種問題:
def get_data_correct(data):
try:
sum_vol = []
for d in data['data']:
sum_vol.append(d['vol'])
return sum(sum_vol)
except KeyError:
# 如果 vol 被換成了 volume
sum_vol = []
for d in data['data']:
sum_vol.append(d['volume'])
return sum(sum_vol)
except TypeError:
# 如果 vol 的數值 value 被換成了 字串 value
# 例如 "vol": 123 -> "vol": "123"
sum_vol = []
for d in data['data']:
sum_vol.append(float(d['vol']))
return sum(sum_vol)
try:
fh = open("ourfile_123", "r")
print("read doc: {}".format(fh))
except IOError:
print('file write failed: {}'.format(IOError))
else:
print('file write done.')
fh.close()
結果:
file write failed: <class 'OSError'>
這類很常見但是通常 pycharm 都幫你做了,原則上不需要特別寫。
try:
print(INPUT) # 使用者丟入的參數動作
except TypeError as e:
# 如果使用 ("1"+1) 會得到:
# can only concatenate str (not "int") to str
# 因為python認為這是 字串要去拼接另一個字串
# 如果使用 (1+"1") 會得到:
# unsupported operand type(s) for +: 'int' and 'str'
# python 認為這是數字要去加一個字串
print(e)
except IndexError as e: # 超出 list 的序列範圍
# 例如 aaa = [1,2,3,4] 但跟程式要 aaa[7]
print(e)
except KeyError as e: # 字典裡不存在的key
# kkk = {
# "a1": 1,
# "a2": 2,
# "a3": 3,
# "a4": 4
# }
# 但跟程式說要執行 kkk['b]
print(e)
except ValueError as e:
# 詳見"仔細說明1"
# 這邊很常發生在 int('5.0') 這種結果下
# 這種問題正確解法是 int(float('5.0'))
print(e)
except ZeroDivisionError as e:
# 3/0 時會發生(分母等於0時)
print(e)
except Exception as e:
# 詳見"仔細說明2"
print(e)
這問題很常發生在各式型別轉來轉去的時候,雖然我們很習慣將 int(“30”)轉為 30 的數值,
但其實在 “5.0”時,python 對於轉型別的自動調整中,是必須先轉成浮點數 float,才能轉成整數 int 哦。
這個雖然很常用,但因為這種做法往往只是捕捉錯誤,而且是很廣泛的捕捉,故比較常使用在無法精確控制的錯誤中,
而且這種做法在系統越來越大之後,反而會因為很不精確的捕獲,進而造成 debug 追溯問題的混輪情形。
或許有人會問說:「為何上面的 def get_data_correct(data): 做法要做三次一樣的?」
這邊可以給各位一個理想中的範例:
def get_data_failed(data):
sum_vol = []
for d in data['data']:
try:
sum_vol.append(d['vol'])
except TypeError as e:
print(e)
return sum(sum_vol)
這樣就好了為何需要做三次一樣的 sum_vol = [] 以及底下的動作呢?
原因是因為發生錯誤的其實是在 sum_vol 這個資料儲存過程內,可是過程中又包含了for 迴圈,這時候如果中斷,那原本的資料則會卡在一半,for 迴圈也跑到一半。
所以如果要對整個行為進行錯誤處理,則會需要將全部的動作們都包到一個 try 內,好讓程式在出錯時,可以直接安全地跳出來哦!
雖然這邊看似講了不少,但其實錯誤處理還有很多類型與處理方式,像是主動發起錯誤、自訂錯誤類型與回傳內容。
乃至於 assert 的用法,都各有其錯誤處理的使用情境,對於是否要放錯誤處理,以及怎麼處理,這個是個大議題,展開討論文章就寫不完啦!不過大致上可以理解成寫程式的重點在於追求正確,
錯誤處理就留給那些自己無法控制的情形就好囉!這章節大概就到這邊,如果有任何想要提問討論的,都歡迎下方留言哦!