程式設計中經常會需要使用到異常處理的情況,在閱讀了一些資料後,整理了關於異常處理的一些小技巧記錄如下。
如何自定義異常
定義異常類
在實際程式設計中,有時會發現Python提供的內建異常的不夠用,我們需要在特殊業務場景下的異常。這時就需要我們來定義自己的異常。按照Python約定俗成的習慣,使用者定義的異常一般都是繼承於Exception類,由它開始擴充。後面我們可以看到這樣做在捕獲異常的時候會帶來很大的便利。
1 2 3 4 5 6 7 |
>>> class MyError(Exception): pass >>> raise MyError(u"something error") Traceback (most recent call last): File "<stdin>", line 1, in <module> __main__.MyError: something error |
API異常相關的技巧
API的異常分為定義異常與呼叫API時如何捕獲異常兩個部分,這二者相輔相成。
定義API異常的技巧
在自己編寫API的時候,應該定義Root Exception——API中的根異常,其它異常都繼承於它。這樣的做法有兩個好處:
- API程式碼層次更清晰
- API與呼叫程式程式碼隔離
假設存在如下場景:需要做一個連結資料庫服務的模組。提供一個connect
函式用於連結。那麼,在連結的過程中,就會發生以下幾種情況:
- socket連線超時
- socket拒絕連線
針對以上的情況,我們在模組中定義幾個異常:
1 2 3 4 5 6 7 8 9 10 11 12 |
# database.py class Error(Exception): """Root exception for all exceptions raised by this module.""" class SocketTimeError(Error): pass class SocketRefuseError(Error): pass def connect(): pass |
呼叫API時異常捕獲的技巧
這樣在呼叫API的時候就可以這樣使用:
1 2 3 4 5 6 7 8 9 10 |
try: connect() except SocketTimeError as err: log.error(err) except SocketRefuseError as err: log.error(err) except Error as err: log.error("API Unexpected error:%s" % err) except Exception: log.error("API bug cause exception.") |
這樣精確定義多個異常,使得程式碼層次清晰,增強了可讀性。值得注意的是:在程式碼的最後還捕獲了Error以及Exception兩個異常,這兩個操作分別對應於可擴充性與健壯性的目的。
捕獲Root Exception以提高可擴充性:
我們知道,在實際連結資料庫時,還可能會出現使用者沒有登陸許可權等問題。所以,我們需要在下一個版本中加入PermissionDeny這個異常。但是,舊的呼叫程式碼已經寫好了,如果忘記修改的話,這個異常可能就會無法被處理,進而使得呼叫的程式奔潰。處於這樣的考慮,我們在呼叫API的時候,就應該再捕獲API的Root Exception,即使之後新加入了其它的異常,在這一個except中也能被捕獲而不影響呼叫程式。使得API模組的可擴充性得到了提高。
捕獲Exception以提高健壯性:
在呼叫API的時候,難免可能出現API內部存在bug的情況。這個時候如果捕獲了Exception的話,就算API內部因為bug發生了異常,也不會影響到呼叫程式的正常執行。
從這兩點中可以看出,要達到這種效果,其實都要依賴於常規異常繼承於Exception類這個規矩。這樣的架構劃分所帶來的好處是顯而易見的。
與異常相關的程式設計藝術
異常代替返回狀態碼
我們經常需要編寫一些工具類的函式,往往在這些函式的處理流程中,會產生很多的狀態;而這些狀態也是呼叫者需要得到的資訊。很多時候,會用一些具有意義的返回值來表示函式處理的狀態。
比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
def write(content): if isinstance(content, basestring): f_handler = open("file.txt", 'w') try: f_handler.write(context) except Exception: return -2 # write file fail else: return 0 # write file succcess finally: f_hanlder.close() else: return -1 # arg type error |
呼叫程式碼:
1 2 3 4 5 6 7 |
result = write() if result == -1: log.error(u"type error") elif result = -2: log.error(u"write error") else: log.info("ok") |
這種狀態碼的方式使用起來特別的不方便,呼叫者還需要去理解每個狀態碼的意義,帶來其它的學習成本;而且用if-else
結構也不易於後期的程式擴充。所以,我們可以使用觸發異常來代替返回狀態碼,每個異常名其實就包含了狀態的意義在內(命名的藝術),使用起來也更好理解。
使用異常的方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Error(Exception): pass class OpenFileError(Error): pass class WriteContentError(Error): pass def write(content): if isinstance(content, basestring): f_handler = open("file.txt", 'w') try: f_handler.write(context) except Exception: raise WriteContentError finally: f_hanlder.close() else: raise OpenFileError |
呼叫程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 |
try: write() except OpenFileError as e: log.error(e) except WriteContentError as e: log.error(e) except Error: log.error("API Error") except Exception log.error("API Bug") else: log.info("ok") |
結合上面一點提到的使用API時的異常捕獲,使得呼叫程式碼變得更佳靈活。
異常處理與流程控制
錯誤處理很重要,但如果它搞亂了程式碼邏輯,就是錯誤的做法
將異常處理與正常流程控制混為一談時,程式碼是十分醜陋的。我們應該將二者分離,最好的做法就是將異常程式碼塊抽離到另外的函式中。
1 2 3 4 5 6 7 8 |
try: action_a() action_b() action_c() except ActionException as e: log.error(e) else: action_d() |
將異常處理分離:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
def action_executor(): action_a() action_b() action_c() def action(): try: action_executor() except ActionException as e: log.error(e) action() action_d() |
參考資料
- 《clean code》
- 《Python3 OOP》
- 《effective python》
- python官方手冊