異常處理在任何一門程式語言裡都是值得關注的一個話題,良好的異常處理可以讓你的程式更加健壯,清晰的錯誤資訊更能幫助你快速修復問題。在Python中,和不部分高階語言一樣,使用了try/except/finally語句塊來處理異常,如果你有其他程式語言的經驗,實踐起來並不難。
異常處理語句 try…except…finally
例項程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
def div(a, b): try: print(a / b) except ZeroDivisionError: print("Error: b should not be 0 !!") except Exception as e: print("Unexpected Error: {}".format(e)) else: print('Run into else only when everything goes well') finally: print('Always run into finally block.') # tests div(2, 0) div(2, 'bad type') div(1, 2) # Mutiple exception in one line try: print(a / b) except (ZeroDivisionError, TypeError) as e: print(e) # Except block is optional when there is finally try: open(database) finally: close(database) # catch all errors and log it try: do_work() except: # get detail from logging module logging.exception('Exception caught!') # get detail from sys.exc_info() method error_type, error_value, trace_back = sys.exc_info() print(error_value) raise |
總結如下
except
語句不是必須的,finally
語句也不是必須的,但是二者必須要有一個,否則就沒有try
的意義了。except
語句可以有多個,Python會按except
語句的順序依次匹配你指定的異常,如果異常已經處理就不會再進入後面的except
語句。except
語句可以以元組形式同時指定多個異常,參見例項程式碼。except
語句後面如果不指定異常型別,則預設捕獲所有異常,你可以通過logging或者sys模組獲取當前異常。- 如果要捕獲異常後要重複丟擲,請使用
raise
,後面不要帶任何引數或資訊。 - 不建議捕獲並丟擲同一個異常,請考慮重構你的程式碼。
- 不建議在不清楚邏輯的情況下捕獲所有異常,有可能你隱藏了很嚴重的問題。
- 儘量使用內建的異常處理語句來 替換try/except語句,比如
with
語句,getattr()
方法。
丟擲異常 raise
如果你需要自主丟擲異常一個異常,可以使用raise
關鍵字,等同於C#和Java中的throw
語句,其語法規則如下。
1 |
raise NameError("bad name!") |
raise
關鍵字後面需要指定你丟擲的異常型別,一般來說丟擲的異常越詳細越好,Python在exceptions
模組內建了很多的異常型別,通過使用dir()
函式來檢視exceptions
中的異常型別,如下:
1 2 3 4 |
import exceptions # ['ArithmeticError', 'AssertionError'.....] print dir(exceptions) |
當然你也可以查閱Python的文件庫進行更詳細的瞭解。
自定義異常型別
Python中也可以自定義自己的特殊型別的異常,只需要要從Exception
類繼承(直接或間接)即可:
1 2 |
class SomeCustomException(Exception): pass |
一般你在自定義異常型別時,需要考慮的問題應該是這個異常所應用的場景。如果內建異常已經包括了你需要的異常,建議考慮使用內建 的異常型別。比如你希望在函式引數錯誤時丟擲一個異常,你可能並不需要定義一個InvalidArgumentError
,使用內建的ValueError
即可。
經驗案例
傳遞異常 re-raise Exception
捕捉到了異常,但是又想重新引發它(傳遞異常),使用不帶引數的raise
語句即可:
1 2 3 4 5 6 7 8 9 10 |
def f1(): print(1/0) def f2(): try: f1() except Exception as e: raise # don't raise e !!! f2() |
在Python2中,為了保持異常的完整資訊,那麼你捕獲後再次丟擲時千萬不能在raise
後面加上異常物件,否則你的trace
資訊就會從此處截斷。以上是最簡單的重新丟擲異常的做法。
還有一些技巧可以考慮,比如丟擲異常前對異常的資訊進行更新。
1 2 3 4 5 6 |
def f2(): try: f1() except Exception as e: e.args += ('more info',) raise |
如果你有興趣瞭解更多,建議閱讀這篇部落格。
Python3對重複傳遞異常有所改進,你可以自己嘗試一下,不過建議還是同上。
Exception 和 BaseException
當我們要捕獲一個通用異常時,應該用Exception
還是BaseException
?我建議你還是看一下 官方文件說明,這兩個異常到底有啥區別呢? 請看它們之間的繼承關係。
1 2 3 4 5 6 7 8 |
BaseException +-- SystemExit +-- KeyboardInterrupt +-- GeneratorExit +-- Exception +-- StopIteration... +-- StandardError... +-- Warning... |
從Exception
的層級結構來看,BaseException
是最基礎的異常類,Exception
繼承了它。BaseException
除了包含所有的Exception
外還包含了SystemExit
,KeyboardInterrupt
和GeneratorExit
三個異常。
有此看來你的程式在捕獲所有異常時更應該使用Exception
而不是BaseException
,因為另外三個異常屬於更高階別的異常,合理的做法應該是交給Python的直譯器處理。
except Exception as e和 except Exception, e
程式碼示例如下:
1 2 3 4 5 6 |
try: do_something() except NameError as e: # should pass except KeyError, e: # should not pass |
在Python2的時代,你可以使用以上兩種寫法中的任意一種。在Python3中你只能使用第一種寫法,第二種寫法被廢棄掉了。第一個種寫法可讀性更好,而且為了程式的相容性和後期移植的成本,請你也拋棄第二種寫法。
raise “Exception string”
把字串當成異常丟擲看上去是一個非常簡潔的辦法,但其實是一個非常不好的習慣。
1 2 3 4 |
if is_work_done(): pass else: raise "Work is not done!" # not cool |
上面的語句如果丟擲異常,那麼會是這樣的:
1 2 3 4 |
Traceback (most recent call last): File "/demo/exception_hanlding.py", line 48, in raise "Work is not done!" TypeError: exceptions must be old-style classes or derived from BaseException, not str |
這在Python2.4以前是可以接受的做法,但是沒有指定異常型別有可能會讓下游沒辦法正確捕獲並處理這個異常,從而導致你的程式掛掉。簡單說,這種寫法是是封建時代的陋習,應該扔了。
使用內建的語法正規化代替try/except
Python 本身提供了很多的語法正規化簡化了異常的處理,比如for
語句就處理的StopIteration
異常,讓你很流暢地寫出一個迴圈。
with
語句在開啟檔案後會自動呼叫finally
中的關閉檔案操作。我們在寫Python程式碼時應該儘量避免在遇到這種情況時還使用try/except/finally的思維來處理。
1 2 3 4 5 6 7 8 9 10 |
# should not try: f = open(a_file) do_something(f) finally: f.close() # should with open(a_file) as f: do_something(f) |
再比如,當我們需要訪問一個不確定的屬性時,有可能你會寫出這樣的程式碼:
1 2 3 4 5 |
try: test = Test() name = test.name # not sure if we can get its name except AttributeError: name = 'default' |
其實你可以使用更簡單的getattr()
來達到你的目的。
1 |
name = getattr(test, 'name', 'default') |
最佳實踐
最佳實踐不限於程式語言,只是一些規則和填坑後的收穫。
- 只處理你知道的異常,避免捕獲所有 異常然後吞掉它們。
- 丟擲的異常應該說明原因,有時候你知道異常型別也猜不出所以然的。
- 避免在catch語句塊中幹一些沒意義的事情。
- 不要使用異常來控制流程,那樣你的程式會無比難懂和難維護。
- 如果有需要,切記使用finally來釋放資源。
- 如果有需要,請不要忘記在處理異常後做清理工作或者回滾操作。