Python 常見錯誤與處理方式-Python 進階處理(二)

一、前言

Python 其實一直有一個相對弱的部分,就是廣大的生態系與眾多的套件中,因為寫法比較模糊,沒有很多靜態語言的風格,故在執行過程中,如果要知道問題出在哪,就將會是很大的問題。

而在常見的異常處理中,也很難針對錯誤的地方找到真正的錯誤,往往套件越複雜,或是導入的套件越多、層數越多,該功能能抓出準確問題的能力也就越困難。

故這篇文章會向大家介紹常見而且我個人覺得實用的做法,但在一開始,我希望先大概介紹一下(不會實作這件事)單元測試的邏輯與情境,以便讓各位在未來的環境中,對於程式碼 debug、異常處理、維護與修改中,有更深一層的理解,就讓我們開始吧!

(以下概念暫時不懂沒關係,但我還是希望讀者可以先至少知道這件事的存在,未來用到會至少有聽過?)

立即訂閱電子報,掌握最新資訊!

    電子郵件

    有興趣的主題
    量化交易金融知識台灣股市國內期貨海外期貨虛擬貨幣

    有興趣的量化交易軟體/平台
    不清楚MultiChartsTradingViewPythonXQMT4MT5

    還有什麼詢問的?

    【早鳥報名中】元富證券 X 量化通 模擬量化交易競賽

    好富投 1920x400
    好富投 978x258

    點我了解更多資訊


    二、Unit Test

    單元測試(Unit test)是什麼?

    這邊稍微介紹一下單元測試的核心理念,通常我們在跑程式碼的時候,最擔心的就是程式碼出錯,而單元(Unit)這個概念更可以理解成一整包程式碼中的每個部件,小到變數的存取與呼叫,大到整個物件的呼叫與繼承(OOP),都是可以被測試的一環。

    或許有人會問:「我一個簡單的邏輯,為何要寫兩份程式碼;2分鐘的任務,變成5分鐘的理由是啥呢?」

    答案也很簡單,隨著程式碼的更迭、累積,後續維護的可能不是開發者,那系統越來越多功能時,如何保證程式碼的每個功能都能運作正常呢?

    這時後平時所寫好的測試就會是一個非常適合迭代的行為,當然,測試分成很多種這邊不展開講,但可以基本理解的就是,測試的目標是為了維持程式過段時間仍然可以正常運作。即便加了新功能,只要一跑,就會知道過去的舊功能是否仍然可以執行,也更可以知道是否會有預期外的錯誤會發生。

    好了,說了個大概後,就進入的今天的重點,錯誤一旦發生,程式該怎麼辦呢?

    Py 101209161710
    Py 101209161711

    三、異常處理(try…except)

    1. 捕獲異常狀況的處理原則

    對於程式碼的錯誤處理,與其說是針對錯誤去做某些事,不如說是針對可以預期的幾種錯誤去做對應的解決方案,這類型問題在程式運算流程中。

    只要資料來源、存取、處理、甚至於套件的使用與繼承,都不是自己可以完全控制的時候(有時候甚至連自己可以100%控制都得做),基本上都會需要做捕捉異常的事情。

    這部分可以探討的很細,關於 Desgin pattern(設計原則)中有針對該議題提到的相關的細節,這邊不會特別展開說。

    至於到底何時該做,何時不該做,大概的分水嶺可以分成:

    (1)這部分功能、函式等等,是不能出錯的

    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 捕捉到了錯誤,但這樣的捕捉其實是給人看的,程式很難針對其作法做出其他行為,這部分底下會一併解釋。

    (2)可以出錯,但有一定的預期錯誤範圍

    這部分的錯誤比較偏向於在 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))

    但這樣會有幾種問題:

    • 每次資料進來都要做一次型態轉換,那萬一他已經是 float 還要再做一次不是很浪費效能嗎?
    • 如果我們預期了 vol(volume) 的簡寫,有可能在未來變成 volume 呢?
    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)

    2. 幾種常見的捕獲錯誤的狀況

    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'>

    3. 常見的使用情境

    (1)SyntaxError 語法錯誤

    這類很常見但是通常 pycharm 都幫你做了,原則上不需要特別寫。

    (2)TypeError 對象類型與要求不符合

    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)
    

    4. 詳細說明

    (1)int(‘5.0’):

    這問題很常發生在各式型別轉來轉去的時候,雖然我們很習慣將 int(“30”)轉為 30 的數值,

    但其實在 “5.0”時,python 對於轉型別的自動調整中,是必須先轉成浮點數 float,才能轉成整數 int 哦。

    (2)except Exception as e:

    這個雖然很常用,但因為這種做法往往只是捕捉錯誤,而且是很廣泛的捕捉,故比較常使用在無法精確控制的錯誤中,

    而且這種做法在系統越來越大之後,反而會因為很不精確的捕獲,進而造成 debug 追溯問題的混輪情形。

    5. 補充

    或許有人會問說:「為何上面的 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 的用法,都各有其錯誤處理的使用情境,對於是否要放錯誤處理,以及怎麼處理,這個是個大議題,展開討論文章就寫不完啦!不過大致上可以理解成寫程式的重點在於追求正確,

    錯誤處理就留給那些自己無法控制的情形就好囉!這章節大概就到這邊,如果有任何想要提問討論的,都歡迎下方留言哦!

      電子郵件

      有興趣的主題
      量化交易金融知識台灣股市國內期貨海外期貨虛擬貨幣

      有興趣的量化交易軟體/平台
      不清楚MultiChartsTradingViewPythonXQMT4MT5

      還有什麼詢問的?


      量化通粉絲社群,一起討論程式交易!

      加入LINE社群量化交易討論群」無壓力討論與分享!

      加入臉書社團「程式交易 Taiwan」即時獲取實用的資源!

      RoWay
      RoWay

      多年投資經驗的兩岸三地操盤手,曾任海外資產管理公司交易平台的產品經理、與各外商投資公司合作開發各式交易策略與系統。

      擅長用Python執行資料蒐集、整理、分析與交易;也善於用Multicharts、MetaTrader等系統建構並回測期貨、期權、區塊鏈策略進而完成投資組合管理。

      文章: 28

      發佈留言

      發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *