1. 程式人生 > >Python程式設計基礎之九錯誤和異常

Python程式設計基礎之九錯誤和異常

一、簡介

  Python最強大的結構之一就是它的異常處理能力,所有的標準異常都使用類來實現,都是基類Exception的成員,都從基類Exception繼承,而且都在exceptions模組中定義。Python自動將所有異常名稱放在內建名稱空間中,所以程式不必匯入exceptions模組即可使用異常。一旦引發而且沒有捕捉SystemExit異常,程式執行就會終止。  
       異常的處理過程、如何引發或丟擲異常及如何構建自己的異常類都是需要深入理解的。

二、詳解

1、什麼是異常

(1)錯誤

       從軟體方面來說,錯誤是語法或邏輯上的。語法錯誤指示軟體的結構上有錯誤,導致不能被直譯器解釋或編譯器無法編譯。這些錯誤必須在程式執行前糾正。邏輯錯誤可能是由於不完整或是不合法的輸入所致,在其他情況下,還可能是邏輯無法生成、計算或是輸出結果需要的過程無法執行,這些錯誤通常分別被稱為域錯誤和範圍錯誤。
        當Python檢測到一個錯誤時,直譯器就會指出當前流已經無法繼續執行下去。這時候就出現了異常。

(2)異常
        對異常的最好描述是:它是因為程式出現了錯誤而在正常控制流以外採取的行為。這個行為又分為兩個階段:首先是引起異常發生的錯誤,然後是檢測和採取可能的措施階段。
        第一個階段是在發生了一個異常條件(有時候也叫做例外的條件)後發生的。只要檢測到錯誤並且意識到異常條件,直譯器會引發一個異常。引發也可以叫做觸發、引發或者生成。 直譯器通過它通知當前控制流有錯誤發生,Python也允許程式設計師自己引發異常。第二階段是:無論是 Python 直譯器還是程式設計師引發的,異常就是錯誤發生的訊號, 當前流將被打斷,用來處理這個錯誤並採取相應的操作。對異常的處理髮生在第二階段, 異常引發後,可以呼叫很多不同的操作。可以是忽略錯誤(記錄錯誤但不採取任何措施,採取補救措施後終止程式),或是減輕問題的影響後設法繼續執行程式。所有的這些操作都代表一種繼續,或是控制的分支。關
鍵是程式設計師在錯誤發生時可以指示程式如何執行。    
         Python採用了"try/嘗試"塊和"catching/捕獲"塊的概念,而且它在異常處理方面更有"紀律性"。可以為不同的異常建立不同的處理器, 而不是盲目地建立一個"catch-all/捕獲所有"的程式碼。

2、Python中的異常

      不管是通過Python直譯器執行還是標準的指令碼執行,所有的錯誤都符合相似的格式, 這提供了一個一致的錯誤介面。所有錯誤,無論是語意上的還是邏輯上的,都是由於和Python直譯器不相容導致的,其後果就是引發異常。       
        NameError:嘗試訪問一個未申明的變數, 任何可訪問的變數必須在名稱空間裡列出, 訪問變數需要由直譯器進行搜尋,如果請求的名字沒有在任何名稱空間裡找到,那麼將會生成一個NameError異常。
        ZeroDivisionError:除數為零;SyntaxError:Python 直譯器語法錯誤;IndexError:請求的索引超出序列範圍;KeyError:請求一個不存在的字典關鍵字;IOError:輸入/輸出錯誤;AttributeError:嘗試訪問未知的物件屬性。

3、檢測和處理異常

        異常可以通過 try 語句來檢測,任何在try語句塊裡的程式碼都會被監測, 檢查有無異常發生。try 語句有兩種主要形式: try-except和try-finally 。這兩個語句是互斥的, 即只能使用其中的一種。一個try語句可以對應一個或多個except子句,但只能對應一個finally子句,或是一個try-except-finally複合語句。可以使用try-except語句檢測和處理異常,也可以新增一個可選的else子句處理沒有探測到異常的時執行的程式碼,而try-finally只允許檢測異常並做一些必要的清除工作(無論發生錯誤與否)。

(1)try-except語句

        try-except 語句(以及其更復雜的形式)定義了進行異常監控的一段程式碼,並且提供了處理異常的機制。
        try-except 語句語法:
try:
    try_suite                                 # watch for exceptions here監控這裡的異常
except Exception[, reason]:    # exception-handling code異常處理程式碼
    except_suite
        在程式執行時,直譯器嘗試執行try塊裡的所有程式碼,如果程式碼塊完成後沒有異常發生,執行流就會忽略except語句繼續執行。而當except語句所指定的異常發生後,會儲存了錯誤的原因,控制流立即跳轉到對應的處理器(try子句的剩餘語句將被忽略)。

(2)封裝內建函式

       互動操作:把一個用字串表示的數值轉換為正確的數值表示形式,其中float()增加了把字串表示的數值轉換為浮點數的功能,替換以前的string.atof()。 float()對引數要求嚴格,例如如果引數的型別正確(字串),其值不可轉換為浮點數,那麼將引發 ValueError異常;若引數是列表,因為型別不正確,所以引發一個TypeError異常。
def safe_float(obj):
    try:
        retval = float(obj)
    except ValueError:
        retval = 'could not convert non-number to float'
        return retval
(3)帶有多個except 的try語句

     把多個except語句連線在一起, 處理一個try塊中可能發生的多種異常。
except Exception1[, reason1]:
    suite_for_exception_Exception1
except Exception2[, reason2]:
    suite_for_exception_Exception2
    :
        程式首先嚐試執行try子句,如果沒有錯誤,忽略所有的except從句繼續執行。如果發生異常,直譯器將在這一串處理器(except 子句)中查詢匹配的異常, 如果找到對應的處理器,執行流將跳轉到這裡。
         Python支援把except語句串連使用,分別為每個異常型別分別建立對應的錯誤資訊,這樣使用者可以得到更詳細的關於錯誤的資訊。
def safe_float(obj):
    try:
        retval = float(obj)
    except ValueError:
        retval = 'could not convert non-number to float'
    except TypeError:
        retval = 'object type cannot be converted to float'
        return retval

(4)處理多個異常的except語句

except 語句可以處理任意多個異常,但要求異常被放在一個元組裡。
except (Exc1[, Exc2[, ... ExcN]])[, reason]:
    suite_for_exceptions_Exc1_to_ExcN
如:

def safe_float(obj):
    try:
        retval = float(obj)
    except (ValueError, TypeError):
        retval = 'argument must be a number or numeric string'
        return retval

(5)捕獲所有異常

捕獲所有的異常的程式碼:
try:
    pass
except Exception, e:
    # error occurred, log 'e', etc.
或不太推薦的方法是使用裸except子句(無任何錯誤資訊,以後可能會不再支援):
try:
    pass
except:
    # error occurred, etc.

       異常部分內容在Python 2.5有了一些變化,異常被遷移到了new-style class上,啟用了一個新的"所有異常的母親",這個類叫做 BaseException。異常的繼承結構有了少許調整,KeyboardInterrupt和SystemExit被從Exception裡移出,和Exception平級。
- BaseException
    |- KeyboardInterrupt
    |- SystemExit
    |- Exception
        |- (all other current built-in exceptions) 所有當前內建異常
若需要捕獲所有異常,那麼就可以使用新的BaseException:
try:
    pass
except BaseException, e:
    # handle all errors
       注意:避免把大片的程式碼裝入try-except中然後使用pass忽略掉錯誤,可以捕獲特定的異常並忽略它們,或是捕獲所有異常並採取特定的動作.,不要捕獲所有異常,然後忽略掉它們。

(6)異常引數

        異常也可以有引數,異常引發後它會被傳遞給異常處理器。當異常被引發後引數是作為附加幫助資訊傳遞給異常處理器的。雖然異常原因是可選的,但標準內建異常提供至少一個引數,指示異常原因的一個字串。異常的引數可以在處理器裡忽略, 但 Python提供了儲存這個值的語法。要想訪問提供的異常原因,必須保留一個變數來儲存這個引數,把這個引數放在except語句後,接在要處理的異常後面。except 語句的這個語法可以被擴充套件為:
# single exception
except Exception[, reason]:
    suite_for_Exception_with_Argument
# multiple exceptions
except (Exception1, Exception2, ..., ExceptionN)[, reason]:
    suite_for_Exception1_to_ExceptionN_with_Argument
       其中reason將會是一個包含來自導致異常的程式碼的診斷資訊的類例項。異常引數自身會組成一個元組,並存儲為類例項(異常類的例項)的屬性,reason將會是一個Exception類的例項。
       無論reason只包含一個字串或是由錯誤編號和字串組成的元組,呼叫 str(reason) 總會返回一個良好可讀的錯誤原因,因為 reason是一個類例項,這樣其實是呼叫類的特殊方法__str__() 。
       唯一的問題就是某些第三方或是其他外部庫並不遵循標準協議, 推薦在引發自己的異常時遵循異常引數規範,用和已有Python程式碼一致錯誤資訊作為傳給異常的引數元組的一部分。如:若引發一個ValueError,那麼最好提供和直譯器引發ValueError時一致的引數資訊。

(7)在應用使用我們封裝的函式

處理一個檔案,將其作為字串讀入,並用一個日誌檔案跟蹤處理程序。

#!/usr/bin/env python

def safe_float(object):
    'safe version of float()'
    try:
        retval = float(object)
    except (TypeError, ValueError), diag:
        retval = str(diag)
    return retval

def main():
    'handles all the data processing'
    log = open('cardlog.txt', 'w')
    try:
        ccfile = open('carddata.txt', 'r')
    except IOError, e:
        log.write('no txns this month\n')
	log.close()
        return

    txns = ccfile.readlines()
    ccfile.close()
    total = 0.00
    log.write('account log:\n')

    for eachTxn in txns:
        result = safe_float(eachTxn)
        if isinstance(result, float):
            total += result
            log.write('data... processed\n')
	else:
	    log.write('ignored: %s' % result)
    print '$%.2f (new balance)' % (total)
    log.close()

if __name__ == '__main__':
    main()
       其中從檔案中提取資料,這裡的檔案開啟被置於try-except語句段中。內建的isinstance()函式檢查結果型別,檢查safe_float是返回字串還是浮點數,任何字串都意味著錯誤,表明該行不能轉換為數字,同時所有的其他數字可以作為浮點數累加入total,在 main()函式的尾行會顯示最終生成的餘額。
(8)else子句

       在try範圍中沒有異常被檢測到時即結束前沒有引發異常,然後執行else子句。
如:
try:
    function()
except:
    pass
else:
    pass

(9)finally子句

       finally子句是無論異常是否發生、是否捕捉都會執行的一段程式碼。可以將finally僅僅配合try一起使用,也可以和 try-except(else也是可選的)一起使用。
try-except-else-finally 語法的示例:
try:
    A
except MyException:
    B
else:
    C
finally:
    D

(10)try-finally語句

       另一種使用finally的方式是finally單獨和try連用。這個try-finally語句和try-except區別在於它不是用來捕捉異常的。作為替代,它常常用來維持一致的行為,無論 try中是否有異常觸發,finally 程式碼段都會被執行。
try:
    try_suite
finally:          #無論如何都執行
    finally_suite
      當在try範圍中產生一個異常時,會立即跳轉到finally語句段。當finally中的所有程式碼都執行完畢後,會繼續向上一層引發異常。
讀取資料的程式碼:

try:
    ccfile = open('carddata.txt', 'r')
    txns = ccfile.readlines()
    ccfile.close()
except IOError:
    log.write('no txns this month\n')
       其中的缺陷:若按照這樣的順序發生錯誤,開啟成功但出於一些原因readlines()呼叫失敗,異常處理會去繼續執行except中的子句,而不去嘗試關閉檔案。通過try-finally來優化:
ccfile = None
try:
    try:
        ccfile = open('carddata.txt', 'r')
        txns = ccfile.readlines()
    except IOError:
        log.write('no txns this month\n')
finally:
    if ccfile:
        ccfile.close()

另一種可選的實現切換了try-except和try-finally包含的方式:

ccfile = None
try:
    try:
        ccfile = open('carddata.txt', 'r')
        txns = ccfile.readlines()
    finally:
        if ccfile:
            ccfile.close()
except IOError:
    log.write('no txns this month\n')
       上述方法唯一的問題是:當finally中的程式碼引發了另一個異常或由於return、break、continue語法而終止,會丟失原來異常的上下文資訊導致原來的異常無法重新引發,除非也事先儲存。

(11)try-except-else-finally:廚房一鍋端

語法樣式:
try:
    try_suite
except Exception1:
    suite_for_Exception1
except (Exception2, Exception3, Exception4):
    suite_for_Exceptions_2_3_and_4
except Exception5, Argument5:
    suite_for_Exception5_plus_argument
except (Exception6, Exception7), Argument67:
    suite_for_Exceptions6_and_7_plus_argument
except:
    suite_for_all_other_exceptions
else:
    no_exceptions_detected_suite
finally:
    always_execute_suite
其中else和finally都是可選的,而必須至少要有一個except子句。

4、上下文管理

(1)with 語句

       另一個隱藏低層次的抽象的例子是with語句,它在Python 2.6中正式啟用(以前必必須用from __future__ import with_statement來匯入它)。
       with 語句的目的在於從流程圖中把try、except和finally關鍵字和資源分配釋放相關程式碼統統去掉,而不是像try-except-finally那樣僅僅簡化程式碼使之易用。 with 語法的基本用法:
with context_expr [as var]:
    with_suite

file和with一起使用的程式碼片段:
with open('/etc/passwd', 'r') as f:
    for eachLine in f:
         # ...do stuff with eachLine or f...
        這段程式碼試圖開啟一個檔案,如果一切正常,把檔案物件賦值給f;然後用迭代器遍歷檔案中的每一行,當完成時關閉檔案。無論在這一段程式碼的開始、中間還是結束時發生異常,都會執行清理的程式碼,此外檔案仍會被自動的關閉。可以看出Python已經拿走了一堆細節,實際上只是進行了兩層處理:第一,使用者層 ,和in類似所需要關心的只是被使用的物件;第二,物件層,既然這個物件支援上下文管理協議,它乾的也就是"上下文管理"。

5、字串作為異常

        早在Python 1.5前,標準的異常是基於字串實現的。然而,這樣就限制了異常之間不能有相互的關係,這種情況隨著異常類的來臨而不復存在。到1.5後,所有的標準異常都是類了,但程式設計師還是可以用字串作為自己的異常的(建議使用異常類)。在2.5中,觸發字串異常會導致一個警告,2.6中捕獲字串異常會導致一個警告。

6、觸發異常

        有些異常由程式執行期間的錯誤而引發,程式設計師在編寫API時希望遇到錯誤的輸入時觸發異常,為此Python提供了一種機制讓程式設計師明確的觸發異常,即raise語句。

(1)raise語法

       raise 語句支援的引數十分靈活,語法上支援許多不同的格式。rasie一般的用法:raise [SomeException [, args [, traceback]]]。
       第一個引數,SomeExcpetion是觸發異常的名字。如果有,它必須是一個字串、類或例項。如果有其他引數(arg或traceback),就必須提供SomeExcpetion.Python的標準異常。
        第二個符號為可選的args(比如引數,值)來傳給異常,這可以是一個單獨的物件也可以是一個物件的元組。當異常發生時,異常的引數總是作為一個元組傳入。如果args原本就是元組,那麼就將其傳給異常去處理;如果args是一個單獨的物件,就生成只有一個元素的元組(就是單元素元組)。大多數情況下,單一的字串用來指示錯誤的原因。如果傳的是元組,通常的組成是一個錯誤字串,一個錯誤編號,可能還有一個錯誤的地址比如檔案等等。
        最後一項引數traceback,同樣是可選的(實際上很少用它)。如果有的話,則是當異常觸發時新生成的一個用於異常-正常化(exception-normally)的追蹤(traceback)物件。當你想重新引發異常時,第三個引數很有用(可以用來區分先前和當前的位置)。如果沒有這個引數,就填寫None。  

(2)  raise慣用法
        最常見的用法為SomeException是一個類,不需要其他的引數,但如果有的話,可以是一個單一物件引數、一個引數的元組或一個異常類的例項。如果引數是一個例項,可以由給出的類及其派生類例項化(已存在異常類的子集)。若引數為例項,則不能有更多的其他引數。

(3)raise少見的慣用法

        當引數是一個例項 , 該例項若是給定異常類的例項當然不會有問題,然而如果該例項並非這個異常類或其子類的例項時,那麼直譯器將使用該例項的異常引數建立一個給定異常類的新例項。 如果該例項是給定異常類子類的例項, 那麼新例項將作為異常類的子類出現, 而不是原來的給定異常類。
       如果raise語句的額外引數不是一個例項——作為替代,是一個單件(singleton)或元組,那麼將用這些作為此異常類的初始化的引數列表。如果不存在第二個引數或是None,則引數列表為空。
       如果SomeException是一個例項,就無需對什麼進行例項化了。這種情況下,不能有額外的引數或只能是None。異常的型別就是例項的類;也就是說,等價於觸發此類異常,並用該例項為引數比如:raiseinstance.__class__,instance。
 還有一個可選的參量(args)作引數.
         最後,這種不含任何引數的raise語句結構是在Python1.5中新引進的,會引發當前程式碼塊(code block)最近觸發的一個異常。如果之前沒有異常觸發,會因為沒有可觸發的異常而生成一TypeError異常。

(4)raise的不同用法

       由於raise有許多不同格式有效語法(比如:SomeException 可以是類、例項或一個字串),以下總結下rasie 的不同用法:


7、斷言

       斷言是一句必須等價於布林真的判定;此外,發生異常也意味著表示式為假(類似C中前處理器assert巨集),但在Python中它們在執行時構建(與之相對的是編譯期判別)。
        斷言通過 assert 語句實現。可以簡簡單單的想象為raise-if語句(更準確的說是raise-if-not 語句)。測試一個表示式,若返回值是假,觸發異常。
         斷言語句,如果斷言成功不採取任何措施(類似語句),否則觸發AssertionError(斷言錯誤)的異常。assert 的語法如下:assert expression[, arguments]。AssertionError異常和其他的異常一樣可以用try-except語句塊捕捉,但是如果沒有捕捉,它將
終止程式執行而且提供一個traceback。
         如同raise 語句一樣,也可以提供一個異常引數給assert 命令:

>>> try:
...     assert 1 == 0, 'One does not equal zero silly!'
... except AssertionError, args:
...     print '%s: %s' % (args.__class__.__name__, args)
... 
AssertionError: One does not equal zero silly!
assert 如何運作,可以通過函式類似實現(內建的變數__debug__在通常情況下為True):
def assert(expr, args=None):
    if __debug__ and not expr:
        raise AssertionError, args

8、標準異常

      所有的異常都是內建的, 所以它們在指令碼啟動前或在互交命令列提示符出現時已經是可用的了。
      Python內建異常:




        所有的標準/內建異常都是從根異常派生的。目前有3個直接從BaseException派生的異常子類:SystemExit、KeyboardInterrupt和Exception,其他的所有的內建異常都是Exception的子類。
        到了Python2.5,所有的異常的都是新風格(new-style)的類,並且最終都是BaseException的子類。從Python1.5到Python2.4.x,異常是標準的類,它們是字串。從Python2.5開始,不再支援構建基於字串的異常並且被正式的棄用,即不能再觸發一個字串異常了,將不能捕獲它們。還有一個就是所有新的異常最終都是BaseException的子類,以便於它們有一個統一的介面。

9、sys和相關模組

(1)sys模組

        另一種獲取異常資訊的途徑是通過sys模組中exc_info()函式,此功能提供了一個3元組(3-tuple)的資訊。

>>> try:
...     float('abc123')
... except:
...     import sys
...     exc_tuple = sys.exc_info()
... 
>>> print exc_tuple
(<type 'exceptions.ValueError'>, ValueError('invalid literal for float(): abc123',), <traceback object at 0x7ff79c87dc20>)
>>> for eachItem in exc_tuple:
...     print eachItem
... 
<type 'exceptions.ValueError'>
invalid literal for float(): abc123
<traceback object at 0x7ff79c87dc20>
       sys.exc_info()得到的元組中是:exc_type異常類;exc_value異常類的例項;exc_traceback追蹤(traceback)物件。第三項,,是一個新增的追蹤(traceback)物件,這一物件提供了的發生異常的上下文,它包含諸如程式碼的執行幀,異常發生時的行號等資訊。在舊版本中的Python中,這三個值分別存在於sys模組,為sys.exc_type、sys.exc_value和sys.exc_traceback ,但這三者是全域性變數而不是執行緒安全的, 建議用sys.exc_info()來代替。

(2)相關模組

    模組                                          描述
exceptions            內建異常(永遠不用匯入這個模組)
contextlib              為使用 with 語句的上下文物件工具
sys                          包含各種異常相關的物件和函式(見sys.ex)

三、總結

(1)Python異常不僅簡化程式碼而且簡化整個錯誤管理體系,異常處理促使成熟和正確的程式設計。
(2)可以通過建立一個從內建的Exception類繼承的類定義自己的異常,然後使用raise命令引發異常或傳遞異常。
(3)若有不足,請留言,在此先感謝!