Python異常編程技巧

編程中經常會需要使用到異常處理的情況,在閱讀了一些資料後,整理了關於異常處理的一些小技巧記錄如下。

如何自定義異常

定義異常類

在實際編程中,有時會發現Python提供的內建異常的不夠用,我們需要在特殊業務場景下的異常。這時就需要我們來定義自己的異常。按照Python約定俗成的習慣,用戶定義的異常一般都是繼承於Exception類,由它開始拓展。後面我們可以看到這樣做在捕獲異常的時候會帶來很大的便利。

>>> 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中的根異常,其它異常都繼承於它。這樣的做法有兩個好處:

  1. API程式碼層次更清晰
  2. API與調用程式程式碼隔離

假設存在如下場景:需要做一個鏈接資料庫服務的模組。提供一個connect函數用於鏈接。那麼,在鏈接的過程中,就會發生以下幾種情況:

  • socket連接超時
  • socket拒絕連接

針對以上的情況,我們在模組中定義幾個異常:

# 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的時候就可以這樣使用:

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類這個規矩。這樣的架構劃分所帶來的好處是顯而易見的。

與異常相關的編程藝術

異常代替返回狀態碼

我們經常需要編寫一些工具類的函數,往往在這些函數的處理流程中,會產生很多的狀態;而這些狀態也是調用者需要得到的資訊。很多時候,會用一些具有意義的返回值來表示函數處理的狀態。 比如:

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

調用程式碼:

result = write()  if result == -1:      log.error(u"type error")  elif result = -2:      log.error(u"write error")  else:      log.info("ok")    

這種狀態碼的方式使用起來特別的不方便,調用者還需要去理解每個狀態碼的意義,帶來其它的學習成本;而且用if-else結構也不易於後期的程式拓展。所以,我們可以使用觸發異常來代替返回狀態碼,每個異常名其實就包含了狀態的意義在內(命名的藝術),使用起來也更好理解。

使用異常的方式:

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

調用程式碼:

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時的異常捕獲,使得調用程式碼變得更佳靈活。

異常處理與流程式控制制

錯誤處理很重要,但如果它搞亂了程式碼邏輯,就是錯誤的做法

將異常處理與正常流程式控制制混為一談時,程式碼是十分醜陋的。我們應該將二者分離,最好的做法就是將異常程式碼塊抽離到另外的函數中。

try:      action_a()      action_b()      action_c()  except ActionException as e:      log.error(e)  else:      action_d()    

將異常處理分離:

def action_executor():      action_a()      action_b()      action_c()    def action():      try:          action_executor()      except ActionException as e:          log.error(e)    action()  action_d()

參考資料