一文掌握 Python 異常處理的所有知識點

異常處理在任何一門編程語言裏都是值得關注的一個話題,良好的異常處理可以讓你的程序更加健壯,清晰的錯誤信息更能幫助你快速修復問題。在Python中,和部分高級語言一樣,使用了try/except/finally語句塊來處理異常,如果你有其他編程語言的經驗,實踐起來並不難。



什麼是異常?



1.錯誤

從軟件方面來說,錯誤是語法或是邏輯上的。錯誤是語法或是邏輯上的。

語法錯誤指示軟件的結構上有錯誤,導致不能被解釋器解釋或編譯器無法編譯。這些些錯誤必須在程序執行前糾正。

當程序的語法正確後,剩下的就是邏輯錯誤了。邏輯錯誤可能是由於不完整或是不合法的輸入所致;

在其它情況下,還可能是邏輯無法生成、計算、或是輸出結果需要的過程無法執行。這些錯誤通常分別被稱爲域錯誤和範圍錯誤。

當python檢測到一個錯誤時,python解釋器就會指出當前流已經無法繼續執行下去。這時候就出現了異常。

 

2.異常

對異常的最好描述是:它是因爲程序出現了錯誤而在正常控制流以外採取的行爲。

這個行爲又分爲兩個階段:首先是引起異常發生的錯誤,然後是檢測(和採取可能的措施)階段。

第一階段是在發生了一個異常條件(有時候也叫做例外的條件)後發生的。

只要檢測到錯誤並且意識到異常條件,解釋器就會發生一個異常。引發也可以叫做觸發,拋出或者生成。解釋器通過它通知當前控制流有錯誤發生。

python也允許程序員自己引發異常。無論是python解釋器還是程序員引發的,異常就是錯誤發生的信號。

當前流將被打斷,用來處理這個錯誤並採取相應的操作。這就是第二階段。

對於異常的處理髮生在第二階段,異常引發後,可以調用很多不同的操作。

可以是忽略錯誤(記錄錯誤但不採取任何措施,採取補救措施後終止程序。)或是減輕問題的影響後設法繼續執行程序。

所有的這些操作都代表一種繼續,或是控制的分支。關鍵是程序員在錯誤發生時可以指示程序如何執行。

python用異常對象(exception object)來表示異常。遇到錯誤後,會引發異常。

如果異常對象並未被處理或捕捉,程序就會用所謂的回溯(traceback)終止執行



異常處理



捕捉異常可以使用try/except語句。

try/except語句用來檢測try語句塊中的錯誤,從而讓except語句捕獲異常信息並處理。

如果你不想在異常發生時結束你的程序,只需在try裏捕獲它。

語法:

以下爲簡單的try....except...else的語法:

try:
<語句>        #運行別的代碼
except <名字>:
<語句>        #如果在try部份引發了'name'異常
except <名字>,<數據>:
<語句>        #如果引發了'name'異常,獲得附加的數據
else:
<語句>        #如果沒有異常發生

Try的工作原理是,當開始一個try語句後,python就在當前程序的上下文中作標記,這樣當異常出現時就可以回到這裏,try子句先執行,接下來會發生什麼依賴於執行時是否出現異常。

  1. 如果當try後的語句執行時發生異常,python就跳回到try並執行第一個匹配該異常的except子句,異常處理完畢,控制流就通過整個try語句(除非在處理異常時又引發新的異常)。

  2. 如果在try後的語句裏發生了異常,卻沒有匹配的except子句,異常將被遞交到上層的try,或者到程序的最上層(這樣將結束程序,並打印缺省的出錯信息)。

  3. 如果在try子句執行時沒有發生異常,python將執行else語句後的語句(如果有else的話),然後控制流通過整個try語句。

使用except而不帶任何異常類型

你可以不帶任何異常類型使用except,如下實例:

try:
   正常的操作
   ......................
except:
   發生異常則執行此處代碼
   ......................
else:
   沒有異常則執行此處代碼

以上方式try-except語句捕獲所有發生的異常。但這不是一個很好的方式,我們不能通過該程序識別出具體的異常信息。因爲它捕獲所有的異常。

使用except而帶多種異常類型

你也可以使用相同的except語句來處理多個異常信息,如下所示:

try:
   正常的操作
   ......................
except(Exception1[, Exception2[,...ExceptionN]]]):
  發生以上多個異常中的一個,執行這塊代碼
   ......................
else:
   如果沒有異常執行這塊代碼

try-finally 語句

try-finally 語句無論是否發生異常都將執行最後的代碼。

try:
<語句>
finally:
<語句>    #退出try時總會執行
raise

當在try塊中拋出一個異常,立即執行finally塊代碼。

finally塊中的所有語句執行後,異常被再次觸發,並執行except塊代碼。

參數的內容不同於異常。

下面來看一個實例:

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

總結如下:

  1. except語句不是必須的,finally語句也不是必須的,但是二者必須要有一個,否則就沒有try的意義了。

  2. except語句可以有多個,Python會按except語句的順序依次匹配你指定的異常,如果異常已經處理就不會再進入後面的except語句。

  3. except語句可以以元組形式同時指定多個異常,參見實例代碼。

  4. except語句後面如果不指定異常類型,則默認捕獲所有異常,你可以通過logging或者sys模塊獲取當前異常。

  5. 如果要捕獲異常後要重複拋出,請使用raise,後面不要帶任何參數或信息。

  6. 不建議捕獲並拋出同一個異常,請考慮重構你的代碼。

  7. 不建議在不清楚邏輯的情況下捕獲所有異常,有可能你隱藏了很嚴重的問題。

  8. 儘量使用內置的異常處理語句來 替換try/except語句,比如with語句,getattr()方法。



經驗案例



傳遞異常 re-raise Exception
捕捉到了異常,但是又想重新引發它(傳遞異常),使用不帶參數的raise語句即可:

def f1():
    print(1/0)
def f2():
    try:
        f1()
    except Exception as e:
        raise  # don't raise e !!!
f2()

在Python2中,爲了保持異常的完整信息,那麼你捕獲後再次拋出時千萬不能在raise後面加上異常對象,否則你的trace信息就會從此處截斷。以上是最簡單的重新拋出異常的做法。

還有一些技巧可以考慮,比如拋出異常前對異常的信息進行更新。

def f2():
    try:
        f1()
    except Exception as e:
        e.args += ('more info',)
        raise

Python3對重複傳遞異常有所改進,你可以自己嘗試一下,不過建議還是同上。

Exception 和 BaseException

當我們要捕獲一個通用異常時,應該用Exception還是BaseException?我建議你還是看一下 官方文檔說明,這兩個異常到底有啥區別呢? 請看它們之間的繼承關係。

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

代碼示例如下:

try:
    do_something()
except NameError as e:  # should
    pass
except KeyError, e:  # should not
    pass

在Python2的時代,你可以使用以上兩種寫法中的任意一種。在Python3中你只能使用第一種寫法,第二種寫法被廢棄掉了。第一個種寫法可讀性更好,而且爲了程序的兼容性和後期移植的成本,請你也拋棄第二種寫法。

raise “Exception string”

把字符串當成異常拋出看上去是一個非常簡潔的辦法,但其實是一個非常不好的習慣。

if is_work_done():
    pass
else:
    raise "Work is not done!" # not cool

上面的語句如果拋出異常,那麼會是這樣的:

Traceback (most recent call last):
  File "/demo/exception_hanlding.py", line 48, in <module>
    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的思維來處理。

# should not
try:
    f = open(a_file)
    do_something(f)
finally:
    f.close()
# should 
with open(a_file) as f:
    do_something(f)

再比如,當我們需要訪問一個不確定的屬性時,有可能你會寫出這樣的代碼:

try:
    test = Test()
    name = test.name  # not sure if we can get its name
except AttributeError:
    name = 'default'

其實你可以使用更簡單的getattr()來達到你的目的。



最佳實踐



最佳實踐不限於編程語言,只是一些規則和填坑後的收穫。

1.只處理你知道的異常捕獲所異常然後吞掉它們。

2.拋出的異常應該說明原因,有時候你知道異常類型也猜不出所以然的。

3.避免在catch語句塊中幹一些沒意義的事情。

4.不要使用異常來控制流程,那樣你的程序會無比難懂和難維護。

5.如果有需要,切記使用finally來釋放資源。

6如果有需要,請不要忘記在處理異常後做清理工作或者回滾操作。



異常速查表



0.jpg

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章