Shopping Cart

購物車內沒有任何商品。

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

一、前言

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

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

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

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

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

    稱呼

    電子郵件

    以下非必填,但若您願意分享,我們將能推送更精準的內容給您

    投資經驗

    是否為理工科背景、工程師或有寫程式的經驗?

    有興趣的主題
    量化交易台股期貨海外期貨虛擬貨幣美股

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

    想透過量化交易達成甚麼目的?
    不確定自動交易選股回測投資績效量化自己的投資方法想找現成的策略套用

    還有什麼想詢問的?

    20241111 1920 400
    20241111 978 258


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

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


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

    加入Discord 「量化交易討論群」即時獲取實用的資源!

    Write Together 101306261122
    Write Together 101306261121
    RoWay
    RoWay

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

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

    文章: 28

    發佈留言

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