來自《python學習手冊第四版》第七部分,而且本書發布的時候3.1還未發布,所以針對本書的一些知識會有些滯后於python的版本,具體更多細節可以參考python的標准手冊。
一、異常基礎(第32章)
異常時可以改變程序中控制流程的時間。在python中異常會根據錯誤自動的被觸發,也能由代碼觸發和截獲。異常由四個語句處理,第一種由兩種編譯,二最后一張在2.6和3.0之前都是可選的擴展功能。
1、a)try/except:捕獲由python或我們引起的異常並恢復;b)try/finally:無論異常是否發生,執行清理行為;c) raise:手動在代碼中觸發異常;d) assert:有條件的在程序代碼中觸發異常;e)with/as:在2.6和后續版本中實現環境管理器(2.5中是可選的功能)。
2、異常作為第七部分是因為需要了解類,才能編寫異常。不過python的異常處理相當簡單,因為它已經整合到語言本身中,成為另一種高級工具。
3、異常可以在一個步驟內跳至異常處理器,中止開始的所有函數調用而進入異常管理器,在異常處理器中編寫代碼,來響應在適當時候引發的異常。異常是一種結構化的“超級goto”,異常處理器(try語句)會留下標識,並可執行一些代碼。程序前進到某處代碼時,產生異常,因而會使python立即跳到那個標識,而放棄留下該標識之后所調用的任何激活的函數。這個協議提供了一種固有的方式響應不尋常的事件。再者,python會立即跳到處理器的語句代碼(比其他編程語言)更簡單,對於可能會發生失敗的函數的每次調用,就沒有必要檢查這些函數的狀態碼。
4、異常的角色:a)錯誤處理,運行時檢測到程序錯誤時,python會引發異常,可以在程序代碼中捕捉和響應錯誤,或者忽略已發生的異常。如果忽略錯誤,python默認的異常處理行為將啟動:停止程序,打印出錯信息。如果不想使用這種默認行為,就需要寫try語句來捕捉異常然后從異常中恢復:當檢測到錯誤時,python會跳到try處理器,而程序在try之后會重新繼續執行;b)事件通知:異常也可用於發出有效狀態的信息,而不需要在程序間傳遞結果標識位,或者刻意對其進行測試。例如,搜索的程序可能在失敗時引發異常,而不是返回一個整數結果代碼;c)特殊情況處理:有時,發生了某種很罕見的情況,很難調整代碼去處理。通常會在異常處理器中處理這些罕見的情況,從而省去編寫應對特殊情況的代碼;d)終止行為,try/finally語句可確保一定會進行需要的結果運算,無論程序中是否有異常;e)非常規控制流程:因為異常是一種高級的“goto”,它可以作為實現非常規的控制流程的基礎。例如:雖然反向跟蹤不是語言本身的一部分,但是能夠通過python的異常來實現,此外需要一些輔助邏輯來退回賦值語句。
5、和其他的核心語言話題相比,異常對python而言是相當簡單的工具。
6、默認異常處理器:假設有下面的函數:

這個函數只是通過傳入的索引值對對象進行索引運算。在正常運算中,它將返回合法的索引值的結果:

如果要求這個函數對字符串末尾以后的位置做索引運算,當函數嘗試執行obj[index]時,就會觸發異常。python會替序列檢測到超出邊界的索引運算,並通過拋出(觸發)內置的IndexError異常進行報告:

因為沒有刻意的捕捉異常,所以將會一直向上返回到程序頂層,並啟用默認的異常處理器:打印標准出錯信息,這些消息包括引發的異常還有堆棧跟蹤:也就是異常發生時激活的程序行和函數清單。這里的出錯消息是有3.0打印出來;它隨着每個版本略有不同,並且甚至隨着每個交互式shell而有所不同。通過交互模式編寫代碼時,文件名就是"stdin"(標准輸入流),表示標准的輸入流。當在IDLE GUI的交互shell中工作的時候,文件名就是“pyshell”,並且會顯示出源行,當沒有文件的時候,文件的行號在這里沒有太大的意義:

在交互模式提示符環境外啟動的更為現實的程序中,頂層的默認處理器也會立刻終止程序,對簡單的腳本來說,這種行為很有道理,錯誤通常是致命的,而檔期發生時,所能做的就是查看標准出錯消息。
7、捕獲異常。當不想要默認的異常行為,就需要把調用包裝在try語句內,自行捕捉異常:

現在,當try代碼塊執行時觸發異常,python會自動跳至處理器(指出引發的異常名稱的except分句廈門的代碼塊)。像這樣以交互模式進行時,在except分句執行后,我們就會回到python提示符下,在更真實的程序中,try語句不僅會捕捉異常,也會從中恢復執行:

這次,在異常捕捉和處理后,程序在捕捉了整個try語句后繼續執行:這就是我們之所以得到“continuing”消息的原因。這里沒看到標准出錯信息,而程序也將正常運行下去。
8、引發異常。上面的都是讓python通過生成錯誤來引發異常,但是,腳本也可以引發異常,也就是說,異常能有python或程序引發,也能捕捉或忽略。要手動觸發異常,直接執行raise語句。用戶觸發的異常的捕捉方式和python引發的異常一樣。下面的代碼是作為個例子解說:

如果沒有捕捉到異常,用戶定義的異常就會向上傳遞,直到頂層默認的異常處理器,並通過標准的出錯消息終止該程序:

assert語句也可以用來觸發異常,它是一個有條件的raise,主要是在開發過程中用於調試:

9、用戶定義的異常,raise語句觸發python的內置作用域中定義的一個內置異常,這里也可以定義自己的新的異常,它特定與我們自己的程序。用於定義的異常能夠通過類編寫,它繼承自一個內置的異常類:通常這個類的名稱叫做Exception。基於類的異常允許腳本建立異常類型、繼承行為以及附加狀態信息:

10、終止行為。try語句可以說“finally”也就是說,它可以包含finally'代碼塊。這看上去就像是異常的except處理器,但是try/finally的組合,可以定義一定會在最后執行時的收尾行為,無論try代碼塊中是否發生了異常:

這里,如果try代碼塊完成后沒有異常,finally代碼塊就會執行,而程序會在整個try后繼續下去。在這個例子中,這看上去不是很好的語句:似乎也可以直接在函數調用后輸入print,從而完全跳過try:

不過這樣寫的話會有個問題:如果函數調用引發了異常,就永遠到不了print。try/finally組合可以避免這個缺點:一旦異常確實在try代碼塊中發生時,當程序被層層剝開,將會執行finally代碼塊:

這里,沒有看到“after try?”消息,因為當異常發生時,控制權在try/finally代碼塊后中斷了。與其相對比的是,python跳回去執行finally的行為,然后把異常向上傳播到前一個處理器(這里就是頂層的默認處理器)。如果修改這個函數中的調用,使其不觸發異常,則finally程序代碼依然會執行,但是程序就會在try后繼續運行:

在實際應用中,try/except的組合可用於捕捉異常並從中恢復,而try/finally則很方便,用來確保無論try代碼塊內的代碼是否發生了任何異常,終止行為一定會運行。例如,可能使用try/except來捕捉從第三方庫導入的代碼所引發的錯誤,然后以try/finally來確保關閉文件,或者終止服務器連接的調用等行為一定會執行。雖然從概念上說,是用於不同的用途,但是在2.5中,可以在同一個try語句內混合except和finally子句:finally一定會執行,無論是否有異常引發,而且也不管異常是否被except子句捕捉到。下一部分將會介紹,在使用某些類型的對象的時候,2.6和3.0提供了try/finally的一種替代,with/as運行一個對象的環境管理邏輯,來確保終止行為的發生:

雖然這個選項代碼不長,它只是在處理某些對象類型的時候才適用,因此,try/finally是一種更加通用的終止結構。另一方面,with/as還運行啟動操作並且支持用戶定義的環境管理代碼。
二、異常編碼細節(第33章)
這部分相對於前面部分的簡單介紹,這里深入介紹下。特別是將介紹try、raise、assert和with語句背后的細節。雖然語句簡單,但是提供了強大的工具來處理python代碼中的異常。ps:異常的內容近年來有一些變化。從2.5起,finally子句可以同樣出現在try語句以及except和else語句中(此前,它們不能組合)。此外,從3.0和2.6開始,新的with環境管理器語句成為正式的,並且用戶定義的異常現在必須編寫為類實例。此外,3.0支持raise語句和except子句的略微修改的語法。
1、try/except/else語句。該教程把try/except/else和try/finally當成獨立的語句進行介紹,因為它們是不同角色,在2.5以前都是無法合並。在2.5中,except和finally可以混在一個try語句中。try是復合語句,它的最完整的形式如下所示。首先是以try作為首行,后面緊跟着(通常)縮進的語句代碼,然后是一個或多個except分句來識別要捕捉的異常,最后是一個可選的else分句。try、except以及else這些關鍵字會縮進在相同的層次(也就是垂直對齊)。下面是3.0中的一般格式:

在這個語句中,try首行底下的代碼塊代表此語句的主要動作:試着執行的程序代碼。except子句定義try代碼塊內引發的異常的處理器,而else子句(如果寫了的話)則是提供沒發生異常時要執行的處理器。在這里的<data>元素和raise語句功能有關。當try語句啟動時,python會標識當前的程序環境,這樣一來,如果有異常發生時,才能返回這里。try首行下的語句會先執行。接下來發生的事情,取決於try代碼塊語句執行時是否引發異常:a)如果try代碼塊語句執行時的確發生了異常。python就跳回try,執行第一個符合引發異常的except子句下面的語句、當except代碼塊執行后(除非except代碼塊引發了另一異常),控制權就會到整個try語句后繼續執行;b)如果異常發生在try代碼塊內,沒有符合的except子句,異常就會向上傳遞到程序中的之前進入的try中,或者如果它是第一條這樣的語句,就傳遞到這個進程的頂層;c)如果try首行底下執行的語句沒有發生異常,python就會執行else行下的語句(如果有的話),控制權會在整個try語句下繼續。try代碼塊語句可以調用寫在程序其他地方的函數,異常的來源可能在try語句自身之外。
2、try語句分句。編寫try語句時,有些分句可以在try語句代碼塊后出現。表33-1列出所有可能形式:

至少會使用其中一種,之前介紹的比如,except分句會捕捉異常,finally分句最后一定會執行,而如果沒遇上異常,else分句就會執行。在2.4中finally必須單獨出現(沒有else和except)。當見到raise語句時,就會探索具有額外數據的部分,它們提供了對作為異常引發的對象的訪問。表33-1中第一和第四項是新的:a)except子句沒列出異常名稱時(except:),捕捉沒在try語句內預先列出的所有異常;b)except子句以括號列出一組異常[except (e1,e2,e3):]會捕捉所有列出的任何異常。
ps:注意當try的子句沒有發生異常的時候,最后的else才會執行,如果有了異常,那么else子句就不會執行。如果想要捕捉任何異常,空的except就能做到,可是當系統離開調用時,也會觸發異常,這些異常是意料之外的系統異常,通常不想去捕獲。這里3.0中有個替代解決方案,捕獲一個名為Exception的異常幾乎與一個空的except具有相同的效果,但是,忽略和系統退出相關的異常:

ps:版本差異提示:3.0要求表33-1中列出的except E as V:處理器子句形式,而不是舊的except E,V:形式,后者在2.6中仍然使用:如果使用它,會轉換為前者形式。這兩種替代形式在2.6中相應的編碼為except(E1,E2):。由於3.0只支持as形式,不管是否使用圓括號,值都會解釋為替代的異常以供捕捉。這一修改改變了作用域規則:使用新的as語法,變量V在except語句塊的末尾刪除。
3、例子:默認行為。假設有廈門的模塊bad.py:

程序忽略它觸發的異常,python會終止這個程序,打印一個消息:

在3.0的一個shell窗口中運行它,消息包含了一個堆棧跟蹤(“Traceback”)以及所引發的異常的名稱和細節。堆棧跟蹤按照從舊到新的順序列出異常發生是激活狀態下的所有程序的行。因為不是在交互模式提示符下工作,所以這種情況下文件和行號信息都很有用。就更大型的調試工作而言,可以用try語句捕捉異常,或者使用第3章所介紹而且會在第35章接着說的調試工具,例如pdb標准庫模塊。
4、有的時候不想在python引發異常時造成程序終止,只要把程序邏輯包裝在try中進行捕捉就行了,這是網絡服務器這類程序很重要的功能,因為它們必須不斷持續運行下去。例如,下面程序代碼在python引發TypeError時就立刻予以捕捉並從中恢復,當時正試着把列表和字符串給鏈接起來(+運算符預期的是兩邊都是相同類型的序列):

當異常在函數kaboom中發生時,控制權會跳至try語句的except分句,來打印消息。因為像這樣異常捕捉后就“死”了,程序會繼續在try后運行,而不是被python終止:

ps:一旦捕捉了錯誤,控制權會在捕捉的地方繼續下去(try之后),沒有直接的方式可以回到異常發生的地方(這里,就是函數kaboom中)。總之,這會讓異常更像是簡單的跳躍,而不是函數調用:沒有辦法回到觸發錯誤的代碼。
5、try/finally。a)如果try代碼塊沒有異常,那么跳至finally代碼塊,然后再整個try語句后繼續執行下去;b)如果try有異常,依然會回來運行finally代碼塊,但是接着會把異常向上傳遞到較高的try語句或頂層默認處理器。程序不會在try語句下繼續執行。也就是說,即使發生了異常,finally代碼塊還是會執行,可except不同的是,finally不會終止異常,而是在finally代碼塊執行后,一直處於發生狀態。
6、接5的例子。下面是一個更為實際的例子,示范了這個語句的典型角色:

這段代碼中,finally包含了一個文件處理函數的調用。確保不論怎么樣該文件總是會關閉。這樣以后的代碼就可以確保文件的輸出緩存去的內容已經從內存轉移到磁盤了。類似的代碼結構可以保證服務器鏈接已關閉了。當這里的函數引發異常,控制流程就會跳回,執行finally代碼塊並關閉文件。然后異常要么會傳遞到另一個try,要么就是傳遞至默認的頂層處理器,絕不會運行到try后的語句。如果在這里的函數沒有引發異常,程序依然會執行finally代碼塊來關閉文件,但是,接着就是繼續運行整個try之后的語句。在2.6和3.0中,異常都是類的實例。
7、統一try/excetp/finally語句:

上面是形式的語句。ps:如果main-action中沒發生異常,而在else-block中發生了,finally還是會執行的,只是執行完之后控制權跳轉到其他地方(另一個try或者默認的頂層處理器)。
8、統一try語句語法:

其中,else和finally是可選的,可能會有0個或多個except,但是,如果出現一個else的話,必須至少有一個except。實際上,該try語句包含兩個部分:帶有一個可選的else的except,以及(或)finally。下面的方式更准確的描述了這一組合的語句語法形式(方括號表示可選,星號表示0個或多個):

由於這些規則,只有至少有一個except的時候,else才能夠出現,並且總是可能混合except和finally,而不管是否有一個else,也可能混合finally和else,但只有在一個except也出現的時候(盡管except可能會忽略一個異常名以捕獲所有的並運行一條raise語句,以重新引發當前的異常)。如果違反了這些順序規則中的任意一條,在代碼運行之前,python將會引發一個語法錯誤異常。
9、通過嵌套合並finally和except。這里的是2.5之前的技術:

相比較於當前的來說,比較晦澀難懂,所以推薦當前的技術。
10、合並try的例子。下面是文件mergedexc.py的四種常見的場景,通過print語句來說明其意義:


下面是在3.0中的輸出結果:

這個例子使用main-action里的內置表達式,來觸發異常(或不觸發),而且利用了python總是會在代碼運行時檢查錯誤的事實。下面介紹如何手動引發異常。
11、raise語句。這是顯式的觸發異常,raise語句的組成是:raise關鍵字,后面跟着可選的要引發的類或者類的一個實例:

在2.6和3.0中,異常總是類的實例。因此,這里第一個raise形式是最常見的,直接提供一個實例,要么是在raise之前創建的,要么是在raise語句中自帶的。如果傳遞一個類,python調用不帶構造函數參數的類,以創建被引發的一個實例:這個格式等同於在類引用后面添加圓括號。最后的形式重新引發最近引發的異常;它通常用於異常處理器中,以傳播已經捕獲的異常。比如下面的兩個例子,對於內置異常,下面兩種形式是對等的,都會引發指定的異常類的一個實例,但是,第一種形式隱式的創建實例:

也可以提前創建實例,因為raise語句接受任何類型的對象引用,如下兩個就像前面兩個一樣引發了IndexError:

當引發一個異常的時候,python把引發的實例與該異常一起發送。如果一個try包含一個名為except name as X:子句,變量X將會分配給引發中所提供的實例:

as在try處理器中是可選的(如果忽略它,該實例直接不會分配給一個名稱),但是,包含它將使得處理器能夠訪問實例中的數據以及異常類中的方法。這種模式對於用類編寫的用戶定義的異常也同樣有效,例如,下面的代碼,傳遞異常類構造函數參數,該參數通過分配的實例在處理器中變得可用:

不管如何指定異常,異常總是通過實例對象來識別,並且大多數時候在任意給的的時刻激活。一旦異常在程序中某處由一條except子句捕獲,它就死掉了,除非由另一個raise語句或錯誤重新引發它。
12、利用raise傳遞異常。raise語句不包括異常名稱或額外數據值時,就是重新引發當前異常。如果需要捕捉和處理一個異常,又不希望異常在程序代碼中死掉時,一般會使用下面的形式:

通過這種方式執行raise時,會重新引發異常,並將其傳遞給更高層的處理器。
13、3.0的異常鏈:raise from。3.0(不是2.6)也允許raise語句擁有一個可選的from子句:

當使用from的時候,第二個表達式指定了另一個異常類或實例,它會附加到引發異常的__cause__屬性。如果引發的異常沒有捕獲,python把異常也作為標准出錯信息的一部分打印出來:


當在一個異常處理器內部引發一個異常的時候,隱式的遵從類似的過程:前一個異常附加到新的異常__context__屬性,並且如果該異常未捕獲的話,再次顯示在標准出錯消息中。ps:3.0不再支持raise Exc, Args形式。該形式在2.6中仍然可用,在3.0中使用raise Exc(Args)示例創建調用形式。2.6中等價的逗號形式是遺留的語法,為了與現在廢棄的基於字符串的異常類型兼容,並且在3.0中也是廢棄的。如果使用的話,會轉換成3.0的調用形式。正如前面看到的,一個raise Exc 形式總是允許的,它在兩個版本中都會轉換為raiseExc()形式,調用無參數的類構造函數。
14、assert語句。assert可視為條件式的raise語句,該語句形式為:

執行起來就像如下的代碼:

如果test計算為假,python就會引發異常:data項(如果提供了的話)是異常的額外數據。就像所有異常,引發的AssertionError異常如果沒有被try捕捉,就會終止程序,在此情況下數據項將作為出錯消息的一部分顯示。assert語句是附加的功能,如果使用-0 python命令行標識位,就會從程序編譯后的字節碼中移除,從而優化程序。AssertionError是內置異常,而__debug__標志位是內置變量名,除非有使用-0標志,否則自動設為1(真值)。使用類似python-0 main.py的一個命令行來在優化模式中運行,並關閉assert。
15、例子:手機約束條件(但不是錯誤)。assert通常是用來作為驗證開發期間程序狀況的,顯示時,其出錯消息正文會自動包括源代碼的行信息,以及列在assert語句中的值,考慮文件asserter.py:

assert幾乎都是用來收集用戶定義的約束條件,而不是捕捉內在的程序設計錯誤。因為python會自行收集程序的設計錯誤。通常來說,沒必要寫assert去捕捉超出索引值,類型不匹配以及除數為零之類的事情:

這類assert一般是多余的。因為python會在遇見錯誤時自動引發異常,讓python來做。另一個例子在第28章的抽象超類例子里。
16、with/as環境管理器。在2.6和3.0中引入了一種新的異常相關的語句:with及其可選的as子句。這個語句的設計是為了和環境管理器對象(支持新的方法協議)一起工作。這一功能在2.5中也可選的使用,用一條如下形式的import來激活:

簡單來說,with/as語句的設計是作為常見的try/finally用法模式的替代方案。就像try/finally語句,with/as語句也是用於定義必須執行的終止或“清理”行為,無論處理步驟中是否發生異常。不過和try/finally不同的是,with語句支持更豐富的基於對象的協議,可以為代碼塊定義支持進入和離開動作。python以環境管理器強化一些內置工具,例如,自動自行關閉的文件,以及對鎖的自動上鎖和解鎖,同時可以用類編寫自己的環境管理器。
17、基本使用:with語句的基本格式如下:

這里的expression要返回一個對象,從而支持環境管理協議。如果選用as子句存在時,此對象也可返回一個值,賦值給變量名variable。ps:variable並非賦值為expression的結果。expression的結果是支持環境協議的對象,而variable則是賦值為其他的東西。然后,expression返回的對象可在with-block開始前,先執行啟動程序,並且在該代碼塊完成后,執行終止程序代碼,無論該代碼塊是否引發異常。有些內置的python對象已得到強化,支持了環境管理協議,因此可以用於with語句。例如,文件對象有環境管理器,可在with代碼塊后自動關閉文件,無論是否引發異常:

在這里,對open的調用,會返回一個簡單文件對象,賦值給變量名myfile。可以用一般的文件工具來使用myfile:文件迭代器會在for循環內逐行讀取。然而,此對象也支持with語句所使用的環境管理協議。在這個with語句執行后,環境管理機制保證由myfile所引用的文件對象會自動關閉,即使處理該文件時,for循環引發了異常也是如此。盡管文件對象在垃圾回收時自動關閉,然而,並不總是能夠很容易的知道會何時發生。with語句的這種用法作為一種替代,允許我們確定在一個特定代碼塊執行完畢后會發生關閉。正如前面看到的,可以使用更同樣而明確的try/finally語句來實現類似的效果,但是,這需要4行管理代碼而不是1行:

這里不討論python的多線程模塊,但是這些模塊所定義的鎖和條件變量同步對象也可以和with語句一起使用,因為它們支持環境管理協議:

在這里,環境管理機制保證鎖會在代碼塊執行前自動獲得,並且一旦代碼塊完成就釋放,而不管異常輸出是什么。decimal模塊(第5章)也使用環境管理來簡化存儲和保存當前小數配置環境(定義了賦值計算時的精度和取整的方式):

在這條語句運行后,當前線程的環境管理器狀態自動恢復到語句開始之前的狀態。要使用try/finally做到同樣的事情,需要提前保存環境並手動恢復它。
18、環境管理協議。可以自行編寫環境管理器。要實現環境管理器,使用特殊的方法來接入with語句,該方法屬於運算符重載的范疇。用在with語句中對象所需的接口有點復雜,而多數程序員只需知道如何使用現有的環境管理器,不過,對那些可能想寫新的環境管理器的工作創造者來說,可以先快速瀏覽其中的細節:
以下是with語句實際的工作方式:a)計算表達式,所得到的對象稱為環境管理器,它必須有__enter__和__exit__方法;b)環境管理器的__enter__方法會被調用。如果as子句存在,其返回值會賦值給as子句中的變量,否則,直接丟棄;c)代碼塊中嵌套的代碼會執行;d)如果with代碼塊引發異常,__exit__(type,value,traceback)方法就會被調用(帶有異常細節)。這些也是由sys.exc_info返回的相同值。如果此方法返回值為假,則異常會重新引發。否則,異常會終止。正常情況下異常是應該被重新引發,這樣的話才能傳遞到with語句之外;e)如果with代碼塊沒有引發異常,__exit__方法依然會被調用,其type,value以及traceback參數都會以None傳遞。
19、接18。這里是一個協議的示范。下面定義一個環境管理器對象,跟蹤其所用的任意一個with語句內with代碼塊的進入和退出:

ps:這個類的__exit__方法返回False來傳播該異常。刪除那里的return語句也有相同效果,因為默認的函數返回值None,按定義也是False。此外,__enter__方法返回self,因為默認的函數返回值None,按定義也是False。此外,__enter__方法返回self,作為賦值給as變量的對象。在其他情況下,這里可能會返回完全不同的對象。運行時,環境管理器以__enter__和__exit__跟蹤with語句代碼塊的進入和離開。如下是實際在3.0中運行的腳本(也能夠在2.6中運行,但是會打印出額外的元組圓括號):

ps:參考標准手冊來了解更多細節,新的contextlib標准模塊提供其他工具來編寫環境管理器。ps:(本書發布的時候3.1還未發布)在即將發布的3.1中,with語句也可以使用新的逗號語法指定多個(有時候叫做嵌套)環境管理器。例如,在下面的例子中,當語句塊退出的時候,兩個文件的退出操作都會自動運行,而不管異常輸出什么:

可以列出任意數目的環境管理器項,並且多個項目和嵌套的with語句一樣的工作。通常python3.1(及其之后的版本)代碼:

等同於如下的代碼,它們在3.1,3.0和2.6下都有效:

三、異常對象(第34章)
python把異常的概念一般化,在2.6和3.0中,內置異常和用戶定義的異常都可以通過類實例對象來標識,雖然這表示必須使用面向對象編程來在程序中定義新的異常,類和oop通常提供了幾個優勢:a)提供類型分類,對今后的修改有了更好的支持,以后增加新的異常時,通常不需要在try語句中進行修改;b)它們附加了狀態信息,異常類提供了存儲在try處理器中所使用的環境信息的合理地點:這樣的話,可以擁有狀態信息以及可調用的方法,並且可以通過實例進行讀取;c)它們支持繼承,基於類的異常允許參與繼承層次,從而可以獲得並定制共同的行為,例如,繼承的顯示方法可提供通用的出錯消息的外觀。因為有這些差異,所以基於類的異常支持了程序的演進和較大系統,所有內置異常都是類組織成繼承樹。在3.0中,用戶定義的異常繼承自內置異常超類,由於這些超類為打印和狀態保持提供了有用的默認值,所以編寫用戶定義的異常的任務也涉及理解這些內置超類的作用。ps:2.6和3.0都要求異常通過類來定義,此外,3.0要求異常類派生自BaseException內置異常超類,而不管是直接還是間接,大多數程序都繼承自這個類的Exception子類,以支持針對常規異常類型的全捕獲處理器,在一個處理器中指定它將會捕獲大多數程序應該捕獲的所有內容,2.6也允許獨立的標准類來充當異常,但是它要求新式類派生自內置異常類,這與3.0相同。
1、在2.6和3.0之前有兩種不同的方式來定義異常,現在只有一種:它從語言中刪除了為了實現向后兼容而保留的大量冗余內容。由於舊的方式有助於說明為什么異常稱為今天的樣子,而且也真的不可能完全擦除上百萬人使用了近二十年的教程的歷史。這里先看看異常的過去。
2、接1,在2.6和3.0之前,可以使用類實例和字符串對象來定義異常。基於字符串的異常在2.5中就發布了廢棄警告,並且在2.6和3.0中刪除了。(所以不要用這部分)字符串異常很容易使用,任何字符串都可以做到,它們根據對象標識來匹配,而不是根據值(也就是說,使用is,而不是==):

對於較大的程序和代碼維護來說,這種形式並不像類那么好,所以放棄了。
3、基於類的異常。字符串是定義異常的簡單方式,相比較於類多了一些優點,最主要的是,類可以讓組織的異常分類,使用和維護更靈活。再者,類可附加異常的細節,而且支持繼承。字符串異常和類異常的主要差別在於,引發的異常在try語句中except子句匹配時的方式不同:
a)字符串異常以簡單對象識別來匹配:引發的異常是由python的is測試來匹配except子句的;
b)類異常是由超類關系進行匹配的:只要except子句列舉了異常的類或其任何超類名,引發的異常就會匹配該子句。
也就是說,當try語句的except子句列出一個超類時,就可以捕捉該超類的實例,以及類樹中所有較低位置的子類的實例,結果就是,類異常支持異常層次的架構:超類變成分類的名稱,而子類變成這個分類中特定種類的異常。except子句列出一個通用的異常超類,就可捕捉整個分類中的各種異常:任何特定的子類都可以匹配;字符串異常沒有這樣的概念:它們是通過簡單的對象標識匹配,沒有直接的方式將它們組織到更為靈活的領域或分組。使得異常處理器以一種難以做出修改的方式與異常集合匹配;基於類的異常也能夠更好的支持了異常狀態信息(附加到實例上),而且可以讓異常參與繼承層次(從而獲得通用的行為)。由於它們提供類和oop一般性的所有優點,比起字符串的異常來說,提供更強大的替代方案。
4、來看看在代碼中類異常是如何應用的,下列classexc.py文件中,定義了一個名為General的超類,以及兩個子類Specific1和Specific2.這個例子說明異常分類的概念:General是分類的名稱,而其兩個子類是這個分類中特定種類的異常。捕捉General的處理器也會捕捉其任何子類,包括Specific1和Specific2:


這段代碼相當直接,不過有些實現細節需要注意:
a)Exception超類。用來構建異常分類樹的類擁有很少的需求,實際上,在上面這個例子中,它們主要是空的,其主體不做任何事情而直接通過。不過,需要注意這里頂層的類是如何從內置的Exception類繼承的。這在3.0中是必需的;2.6也允許獨立的經典類充當異常,但是它要求新式類派生自內置異常類,這和在3.0中一樣。由於Exception提供了一些有用的行為,隨后才會遇到這些行為,因此,在這里不能使用它們;但是,在任何python版本中,從它那里繼承是個好主意;
b)引發實例。在上面代碼中,調用類來創建raise語句的實例。在類異常模式中,總是引發和捕獲一個類實例對象。如果我們在一個raise中列出了類名而沒有圓括號,那么python調用該類而沒有構造函數參數為我們產生一個實例。異常實例可以在該raise之前創建,就像這里所做的一樣,或者在raise語句自身中創建;
c)捕獲分類。這段代碼包含一些函數,引發三個類實例使其成為異常,此外,有個頂層try會調用那些函數,並捕捉General異常(同一個try也會捕捉兩個特定的異常,因為它們是General的子類);
d)異常細節。在下一章詳細說說異常處理器sys.exc_info調用:這是一種抓取最近發生異常的常用方式。簡單來說,對基於類的異常而言,其結果中的第一個元素就是引發異常類,而第二個是實際引發的實例。這里的except子句捕獲了一個分類中所有的類,在這一的一條通用的except子句中,sys.exc_info是決定到底發生了什么的一種方式。在這一特別情況下,它等價於獲取實例的__class__屬性。
5、接4,正如在下一章看到的,sys.exc_info方法通常也捕獲所有內容的空的except子句一起使用。最后一點需要說的是,當捕獲了一個異常,我么可以確定該實例是except中列出的類的一個實例,或者是其更具體的子類中的一個。因此,實例的__class__屬性也給出了異常類型。例如,如下的變體和前面的例子起着相同的作用:

由於__class__可以像這樣使用來決定引發的異常的具體類型,因此sys.exc_info對於空的except子句更有用,否則的話,沒有一種方式來訪問實例及其類。此外,更使用的程序通常根本不必關注引發了哪個具體的異常,通過一般調用實例的方法,我們自動把修改后的行為分派給引發的異常。
6、為什么使用異常。給出廈門的形式,在except子句的括號內列出字符串異常名稱的清單,可以達到和上面一樣的效果:

這種方法對於廢棄的字符串異常模式也是有效的。不過對大型或多層次的異常而言,在一個except子句中使用類捕捉分類,會比列出一個分類中的每個成員更為簡單。此外,可以新增子類擴展異常層次,而不會破壞現有的代碼。例如,這里給出個例子,編寫一個數值計算庫,並給出兩個異常:除數為零以及數值溢出。在文檔中指出這些是異常,可以通過庫引發這兩個異常,同時在代碼中將異常定義為簡單字符串:

現在,當人們使用庫的時候,一般會在try語句內,把對函數或類的調用包裝起來,從而捕捉兩個異常(如果他們沒捕捉異常,庫的異常會終止代碼):

當這樣使用差不多6個月或者多久的時候,你想做修改,在這個過程中,發現比如退位(underflow)也會出錯,所以增加新的一個字符串異常:

不過,也許當發布代碼時,用戶會發現它們如果要明確列出異常,就得回去修改每處調用我們的庫的地方,來引入新增的異常名:

用戶為了方面,編寫了空的except子句來捕捉所有的可能的異常:

可是這樣卻會捕捉到不想要的異常:內存耗盡,鍵盤終端(ctrl-c),系統退出甚至自己的try塊中的代碼錄入錯誤,都將觸發異常。類異常可以完全修復這種難題,不是將庫的異常定義為簡單的一組字符串,而是安排到類樹中,有個共同的超類來包含整個類型:

這樣的話,庫用戶只需列出共同的超類(也就是分類),來捕捉庫的所有異常,無論是現在還是以后:

當回來修改代碼時,作為共同超類的新的子類來增加新的異常:

結果就是用戶代碼捕捉庫的異常依然保持正常工作,沒有改變。事實上,可以在未來任意新增、刪除以及修改異常,只要客戶端使用的是超類的名稱,就和異常集中的修改無關,也就是維護更容易了。
7、要理解上述的這些用法,首先需要看看用戶定義的異常類與它們所繼承自的內置異常是如何相關的。內置Exceptiopn類。python自身能夠引發的所有的內置異常,都是預定義的類對象。此外,內置異常通常通過一般的超類分類以及具體的子類形態組織成的層次,很像之前學過的異常類樹。在3.0中,所有熟悉的異常(例如,SyntaxError)都是預定義的類,可以作為內置變量名,可以作為builtin模塊中的內置名稱使用(在2.6中,位於__builtin__,並且也是標准庫模塊exceptions的屬性)。此外,python把內置異常組織成層次,來支持各種捕捉模式:
a)BaseException.,異常的頂級根類,這個類不能當作是由用戶定義的類直接繼承的(使用Exception)。它提供了子類所繼承的默認的打印和狀態保持行為。如果在這個類的一個實例上調用str內置函數(例如,通過print),該類返回創建實例的時候所傳遞的構造函數參數的顯示字符串(或者如果沒有參數的話,是一個空字符串)。此外,除非子類替代了這個類的構造函數,在實例構造時候傳遞給這個類的所有參數都將作為一個元組存儲於其args屬性中;
b)Exception。與應用相關的異常的頂層根超類。這是BaseException的一個直接子類,並且是所有其他內置異常的超類,除了系統退出事件類之外(SystemExit、KeyboardInterrupt和GeneratorExit)。幾乎所有的用戶定義的類都應該繼承自這個類,而不是BaseException。當遵從這一慣例的時候,在一條try語句的處理器中指明Exception,會確保程序將捕獲除了系統退出事件之外的所有異常,通常該事件是允許通過的。實際上,Exception變成了try語句中的一個全捕獲,並且比一條空的except更精確。
c)ArithmeticError。所有數值錯誤的超類(並且是Exception的一個子類)。
d)OverflowError。識別特定的數值錯誤的子類。
其他等等,可以在python pocket reference或python庫手冊這樣的幫助文本中進一步閱讀詳細內容。注意,異常類樹在3.0和2.6中略有不同。還要注意,只有在2.6中,可以在exception模塊(在3.0中刪除了)的幫助文本中看到類樹:

8、內置異常分類。內置類樹可讓我們選擇處理器具體或通用的程度。例如,內置異常ArithmeticError是如OverflowError和ZeroDivisionError這樣的更為具體的異常的超類。在一條try中列出ArithmeticError,將會捕獲所引發的任何類型的數值錯誤;只列出OverflowError時,就只會攔截這種特定類型的錯誤,而不能捕捉其他的異常。與之類似的是,因為Exception是python中所有應用程序級別的異常的超類,通常可以使用它作為一個全捕獲,其效果與一條空的except很類似,但是它運行系統退出異常而像平常那樣通過:

這在2.6中通常沒用,因為編寫為經典類的獨立的用戶定義異常,不要求必須是Exception根類的子類。這在3.0中不可靠,因為它要求所有的類都派生自內置異常。即便在3.0中,這種方案會像空的except一樣遭遇大多數相同的潛在陷阱,就像前一章說的,他可能攔截用於其他地方的異常,並且可能掩蓋了真正的變成錯誤。
9、默認打印和狀態。內置異常還提供了默認打印顯示和狀態保持,它往往和用戶定義的類所需的邏輯一樣多。除非重新定義了類繼承自它們的構造函數,傳遞給這些類的任何構造函數參數都會保存在實例的args元組屬性中,並且當打印該實例的時候自動顯示(如果沒有傳遞構造函數參數,則使用一個空的元組和顯示字符串)。這說明了為什么傳遞給內置異常類的參數會出現在出錯消息中,當打印實例的時候,附加給實例的任何構造函數參數就會顯示:

對於用戶定義的異常也是如此,因為它們繼承了其內置超類中存在的構造函數和顯示方法:


注意,該異常實例對象並非字符串自身,但是,當打印的時候,使用第29章介紹的__str__運算符重載協議來提供顯示字符串:要連接真正的字符串,執行手動轉換:str(X)+"string"。盡管這種自動狀態和現實支持本身是有用的,但對於特定的顯示和狀態保持需求,總是可以重新定義Exception子類中的__str__和__init__這樣的繼承方法,下面的10介紹如何做到的。
10、定制打印顯示。正如第9中看到的,默認情況下,捕獲並打印基於類的異常的實例的時候,它們會顯示我們傳遞給類構造函數的任何內容:

當沒有捕獲異常的時候,如果異常作為一條出錯消息的一部分顯示,這個繼承的默認顯示模式也會使用:

對於很多用途來說,這已經足夠了。要提供一個更加定制的顯示,可以在類中定義兩個字符串標識重載方法中的一個(__repr__或__str__),來返回想要為異常顯示的字符串。如果異常被捕獲並打印,或者異常到達默認的處理器,方法返回的字符串都將顯示:


這里要注意的一點是,通常為此目的必須重新定義__str__,因為內置的超類已經有一個__str__方法,並且在大多數環境下(包括打印),__str__優先於__repr__.。如過定義了一個__repr__,打印將會很樂意的調用超類的__str__。對於未捕獲的異常,方法返回的內容都包含在出錯消息中,並且打印異常的時候顯示化。這里,方法返回一個硬編碼的字符串來說明,但是,它也可以執行任意的文本處理,可能附加到實例對象的狀態信息。第11部分介紹狀態信息選項。
11、定制數據和行為。除了支持靈活的層級,異常類還提供了把額外狀態信息存儲為實例屬性的功能。正如前面看到的,內置異常超類提供了一個默認的構造函數,它自動把構造函數參數存儲到一個名為args的實例元組屬性中。盡管默認的構造函數對於很多情況都適用,但為了滿足更多的定制需求,可以提供一個自己的構造函數。此外,類可以定義在處理器中使用的方法,來提供預先編碼的異常處理邏輯。
12、接11.當引發一個異常的時候,可能會跨越任意的文件界限,觸發異常的raise語句和捕獲異常的try語句可能位於完全不同的模塊文件中。在一個全局變量中存儲額外的細節通常是不行的,因為try語句可能不知道全局變量位於哪個文件中。在異常自身中傳遞額外的狀態信息,這允許try語句更可靠的訪問它。正如之前看到的,當引發一個異常的時候,python隨着異常傳遞類實例對象。在try語句中的代碼,可以通過在一個except處理器中的as關鍵字之后列出一個額外的變量,來訪問引發的異常。這提供了一個自然的鈎子,以用來為處理器提供數據和行為。例如,解析數據文件的一個程序可能通過引發一個異常實例來標識一個格式化錯誤,而該實例用關於錯誤的額外細節來填充:

在這里的except子句中,對引發異常的時候所產生的實例的一個引用分配給了X變量。這使得能夠通過定制的構造函數來訪問附加給該實例的屬性。盡管可能依賴於內置超類的默認狀態保持,它與我們的應用程序幾乎不相關:

13、提供異常的方法。除了支持特定於應用程序的狀態信息,定制構造函數還更好的支持用於異常對象的額外信息。也就是說,異常類也可以定義在處理器中調用的方法。例如,下面的代碼添加了一個方法,它使用異常狀態信息把錯誤記錄到一個文件中:


運行的時候,這段腳本把出錯消息寫入一個文件中,以響應異常處理器中的方法調用:

在這一的一個類中,方法(如logerror)也可能繼承自超類,並且實例屬性(例如line和file)提供了一個地方來保存狀態信息,狀態信息提供了額外環境用於隨后的方法調用。此外,異常類可以自由的定制和擴展繼承的行為。換句話說,由於它們是用類定義的,所以第六部分的oop的特性,對於異常來說也是可用的。
四、異常的設計(第35章)
這部分包括了異常設計的話題以及常用例子的集合,再加上這一部分的陷阱,(每一部分的最后應該都有python的工具的介紹,比如pydoc什么的)。
1、嵌套異常處理器。上面的例子都只使用了單一的try語句來捕捉異常,如果try中還有try,那么怎樣?也就是try調用一個會執行另一個try的函數,也就是嵌套。python會在運行時將try語句放入堆棧,這樣就能夠理解了。當發生異常時,python會回到最近進入、具有相符except分句的try語句。因為每個try語句都會留下標識,python可檢查堆棧的標識,從而跳回到較早的try。這種處理器的嵌套化,就是之前說的異常向上傳遞至較高的處理器的意思:這類處理器就是在程序執行流程中較早進入的try語句。圖35-1說明了嵌套的try/except語句在運行時所發生的事情。進入try代碼塊的代碼量可能很大(例如,它可能包含了函數調用),而且通常會啟用正在監視相同異常的其他代碼。當異常最終引發時,python會跳回到匹配該異常,最近進入的try語句,執行該語句的xcept分句,然后在try語句后繼續下去。一旦異常被捕捉,其生命就結束:控制權不會跳回所有匹配這個異常、相符的try語句;只有第一個try有機會對它進行處理,如圖35-1所示,函數func2中的raise語句會把控制權返還func1中的處理器,然后程序再在func1中繼續下去:

與之對比的,當try/finally語句嵌套且異常發生時,每個finally代碼塊都會執行:python會持續把異常往上傳遞到其他try語句上,最終可能達到頂層默認處理器。如圖35-2,finally子句不會終止異常,而是指明異常傳播過程中,離開每個try語句之前要執行的代碼。如果異常發生時,有很多try/finally都在活動,它們就都會運行,除非有個try/except在這個過程中捕捉某處該異常:

也就是印發異常時,程序去向何方完全取決於異常在何處發生:這是腳本運行時控制流程的函數,而不僅僅是其語法。異常的傳遞,基本上就是回到處理先前進入但尚未離開的try。只要控制權碰到相符except子句,傳遞就會停止,而通過finally子句時就不會。
2、例子:控制流程嵌套。下面是模塊文件nestexc.py定義了兩個函數。action2是寫成要觸發異常(做數字和序列的加法),而action1把action2調用封裝在try處理器內,以捕捉異常:

那么,文件底端的頂層模塊代碼,也在try處理器中包裝了action1調用,當action2觸發TypeError異常時,就有兩個激活的try語句:一個在action1內,另一個在模塊文件頂層。python會挑選並執行具有相符except,最近try,而在這個例子中就是action1中的try。異常最后的所在之處,取決於程序運行時的控制流程。因此,想要知道要去哪,就知道現在在哪。就這個例子而言,異常在哪進行處理是控制流程的函數,而不是語句的語法。然而,可以用語法把異常處理器嵌套化。
3、例子:語法嵌套化。第33章討論新的統一后的try/except/finally語句時,就像前面提到的,從語法上有可能讓try語句通過其源代碼中的位置來實現嵌套:

這段代碼只是像之前的那個例子一樣(行為也相同),設置了相同的處理器嵌套結構、。實際上,語法嵌套的工作就像圖35-1和35-2所描繪的一樣。唯一的差別就在於,嵌套處理器實際上是嵌入try代碼塊中,而不是寫在其他被調用的函數中,例如,嵌套的finally處理器會因一個異常而全部啟動,無論是語法上的嵌套,或者因運行時流程經過代碼中某個部分:

參考圖35-2有關這段代碼運行的圖形說明。效果是一樣的,不過函數邏輯變成了嵌套語句。有關語法嵌套更有用的例子,可以看下下面的文件except-finally.py:

此代碼在異常引發時,會對其進行捕捉,而且無論是否發生異常,都會執行finally終止動作。其效果就像單個try語句內結合except和finally(在2.5及其以后的版本中):


就像在第33章見到的,2.5時,except和finally子句可以混合在相同try語句中。這也是的本節講的某些語法嵌套變得不再必要,雖然依然可用,但可能是出現在2.5版本以前的代碼中,而且可作為執行其他的異常處理行為的技術。
4、異常的習慣用法:異常不總是錯誤。在python中,所有錯誤都是異常,可是不是所有的異常都是錯誤。例如,在第9章看到,文件對象讀取方法會在文件末尾時返回空字符串。與之對比的是,內置的input 函數在每次調用時,則是從標准輸入串流sys.stdin讀取一行文字,並且在文件末尾時引發內置的E0FError(這一功能在2.6中叫做raw_input)。和文件方法不同的是,這個函數並不是返回空字符串:input的空字符串是指空行。除了E0FError的名稱,這個異常在這種環境下也只是信號而已,不是錯誤。因為有這種行為,除非文檔末尾應該終止腳本,否則,input通常會出現在try處理器內,並嵌入循環內,如下列代碼:

其他內置異常都是類似的信號,而不是錯誤,例如,調用sys.exit()並在鍵盤上按下Ctrl-C,會分別引發SystemExit和KeyboardInterrupt。python也有一組內置異常,代表警告,而不是錯誤。其中有些代表了正在使用不推薦的(即將退出的)語言功能的信號。(可參考庫手冊有關內置異常的說明,以及warning模塊相關的警告)。
5、函數信號條件和raise。用戶定義的異常也可引發非錯誤的情況。例如,搜索程序可以寫成找到相符者時引發異常,而不是為調用者返回裝填標志來攔截。在下面的代碼中,try/except/else處理器做的就是if/else返回值的測試工作:

更通常的情況是,這種代碼結構,可用於任何無法返回警示值(sentinel value)以表明成功或失敗的函數。例如,如果所有對象都是可能的有效返回值,就不可能以任何返回值來代表不尋常的情況。異常提供一種方式來傳達結果信號,而不使用返回值:

因為python核心是動態類型和多態的,所以通常傾向於使用異常來發出這類情況的信號,而不是警示性的返回值。
6、關閉文件和服務器鏈接。確保一個特殊代碼塊的終止操作的更通用和顯式的方式是try/finally語句:

2.6和3.0中的一些對象使得這更為容易:提供由with/as語句運行的環境管理器,從而為我們自動終止或關閉對象:

那么,選哪種好呢?通常,這取決於我們的程序。與try/finally相比,環境管理器更為隱式,它與python通常的設計哲學背道而馳。環境管理器肯定也不太常見,它們通常只對選定的對象可用,並且編寫用戶定義的環境管理器來處理通用的終止需求,比編寫一個try/finally更為復雜。另一方面,使用已有的環境管理器,比使用try/finally需要更少的代碼,如前面的例子所示。此外,環境管理器協議除了支持退出動作,還支持進入動作。盡管try/finally可能是更加廣為應用的技術,環境管理器可能更適合可以使用它們的地方,或者可以允許它們的額外復雜性的地方。
7、在try外進行調試。也可以利用異常處理器,取代python的默認頂層異常處理行為。在頂層代碼中的外層try中包裝整個程序(或對它調用),就可以捕捉任何程序執行時會發生的異常,因此可破壞默認的程序終止行為。下面的代碼中,空的except子句會捕捉任何程序執行時所引發的而未被捕捉到的異常。要取得所發生的實際異常,可以從內置sys模塊取出sys.exc_info函數的調用結果。這會返回一個元組,而元組之前兩個元素會自動包含當前異常的類和引發的實例對象:

這種結構在開發期間經常會使用,在錯誤發生后,仍保持程序處於激活狀態:這樣可以執行其他測試,而不用重新開始。測試其他程序時,也會用到它,就像下一節描述的。
8、運行進程中的測試。可以在測試驅動程序的應用中結合剛才所見到的一些編碼模式,在同一進程中測試其他代碼:

在這里的testdriver函數會循環進行一組測試調用(在這個例子中,模塊testapi是抽象的)。因為測試案例中未被捕捉的異常,一般都會終止這個測試驅動程序,如果想在測試失敗后讓測試進程繼續下去,就需要在try中包裝測試案例的調用。就像往常一樣,空的except會捕捉由測試案例所產生的沒有被捕捉的異常,而其使用sys.exc_info把該異常記錄到文件內。沒有異常發生時(測試成功情況),else分句就會執行。對於作為測試驅動運行在同一個進程的函數、模塊以及類,而進行測試的系統而言,這種形式固定的代碼是很經典的。然而,在實際應用中,測試可能會比這里所演示的更為復雜。例如,要測試外部程序時,要改為檢查程序啟動工具所產生的狀態代碼或輸出。例如,標准庫手冊所談到的os.system和os.open(這一類工具一般不會替外部程序中的錯誤引發異常。事實上,測試案例可能會和測試驅動並行運行)。
9、關於sys.exc_info。sys.exc_info通常允許一個異常處理器捕獲對最近引發的異常的訪問。當使用空的except子句來盲目的捕獲每個異常以確定引發了什么的時候,這種方式特別有用:

如果沒有處理器正在處理,就返回包含了三個None值的元組。否則,將會返回(type、value和traceback):a)type是正在處理的異常的異常類型;b)value是引發的異常類實例;c)traceback是一個traceback對象,代表異常最初發生時所調用的堆棧(參考標准traceback模塊到文檔,來獲得可以和這個對象共同使用的對象工具,從而可以手動產生出錯消息的工具)。正如在前一章看到的,當捕獲異常分類超類的時候,sys.exc_info對於確定特定的異常類型很有用。由於在這種情況下,也可以通過as子句所獲取的實例的__class__屬性來獲得異常類型,sys.exc_info如今主要由空的except使用:

也就是說,使用實例對象的接口和多態,往往比測試異常類型更好的方法,可以為每個類定義異常方法並通用的運行:

通常,在python中太具體可能會限制代碼的靈活性。ps:在2.6中,舊的工具sys.exc_type和sys.exc_value依然可以用於獲得最近異常的類型和值,但是只能替整個進程管理單個的全局異常。這兩個名稱在3.0中已經刪除。新的更傾向於使用的sys.exc_info()調用在2.6和3.0中都可以使用,它記錄每個線程的異常信息,因此是線程專有的方式。當然,當在python中使用多線程時,這種區別才更重要。
10、與異常有關的技巧。在異常這一部分中,大多數常見的陷阱都來自於設計問題,大致來說,python的異常在使用上都很簡單,異常背后的真正的技巧在於,確定except子句要多具體或多通用,以及try語句中要包裝多少代碼。
11、接11,應該包裝什么。從理論上說,可以在腳本中把所有的語句都包裝在try中,但是這樣不明智(這樣的話,try語句也需要包裝在try語句中)。這是設計上的問題,不是語言的問題:
a)經常會失敗的運算一般都應該包裝在try語句內。例如,和系統狀態銜接的運算(文件開啟、套接字調用等)就是try的主要候選者;
b)盡管這樣,上面的規則有些特例:在簡單的腳本中,會希望這類運算失敗時終止程序,而不是被捕捉或者被忽略。如果錯誤更大,那么更要這樣。python中的錯誤會產生有用的出錯信息(如果不是崩潰的話),而且這通常就是所期望的最好結果;
c)應該在try/finally中實現終止動作,從而保證它們的執行,除非環境管理器作為一個with/as選項可用。這個語句的形式可以執行代碼,無論異常是否發生;
d)偶爾,把對大型函數的調用包裝在單個try語句內,而不是讓函數本身零散在諾干try語句內。這樣更方便。這樣的話,函數中的異常就會往上傳遞到調用周圍的try,而也可以減少函數的代碼量。
我們所編寫的程序種類可能會影響處理異常的代碼的量。例如,服務器一般都必須持久的運行,所以,可能需要try語句捕捉異常並從中恢復。不過較簡單的一次性腳本,通常會完全忽略異常的處理,因為任何步驟的失敗都要求關閉腳本。
12、捕捉太多:避免空except語句。空except子句會捕捉try代碼塊中代碼執行時所引發的每一個異常:

捕捉的也包含無關的系統異常,而這些我們卻不需要去關注的也會被捕捉到。例如,當控制權到達頂層文件的末尾時,腳本是正常的退出。然而,python也提供內置sys.exit(statuscode)調用來提前終止。這實際上是引發內置的SystemExit異常來終止程序,使try/finally可以在離開前執行,而程序的特殊類型可攔截該事件。因此,try帶空except時,可能會不知不覺阻止重要的結束,如下面的文件exiter.py:


可能會無法預期運算中可能發生的所有的異常種類。使用前一章介紹的內置異常類,在這種特定情況下會有幫助,因為Exception超類不是SystemExit的一個超類:

在其他情況下,這種方案並不比空的except子句好,因為Exception是所有內置異常(除了系統退出事件以外)之上的一個超類,它仍然有潛力捕獲程序中其他地方的異常。最糟糕的情況是,空except和捕獲Exception類也會捕捉一般程序設計錯誤,但這類錯誤多數時候都應讓其通過的。事實上,這兩種技術都會有效關閉python的錯誤報告機制,使得代碼中的錯誤難以發現。例如,下面的代碼:

這里的代碼的編寫者假設,對字典做字典運算時,唯一可能發生的錯誤就是鍵錯誤。但是,因為變量名myditcionary拼寫錯誤(mydictionary),python會為未定義的變量名的引用引發NameError,但處理器會默默的捕捉並忽略這個異常。事件處理器錯誤填寫了字典錯誤的默認值,導致了程序出錯。如果這件事是發生在離讀取值的使用很遠的地方,就會變成一項很復雜的調試任務。
13、捕捉過少:使用基於類的分類。另一方面,處理器也不能太具體化。當在try中列出特定的異常時,就只捕捉實際所列出的事件,可是如果系統需要演化的時候,需要回頭在代碼的其他地方,加上這些新的異常。在前一章就說過這個問題,例如下面處理器是吧MyExcept1和MyExcept2看作是正常的情況,並把其他的一切視為錯誤。如果未來增加了MyExcept3,就會視為錯誤並對其進行處理,除非更新異常列表:

不過如果和第33章討論過的基於類的異常,可以讓這種陷阱消失,就像見到的,如果捕捉一般的超類,就可以在未來新增和引發更為特定的子類,而不用手動擴展except分句的列表:超類會變成可擴展的異常分類:

這個道理就是異常處理器不要過於一般化,也不要太具體。要明智的選擇try語句所包裝的代碼量,特別是在較大系統中,異常規則也應該是整體設計的一部分。
14、核心語言總結。python工具集。從這里開始,以后的python生涯大部分時間就是熟練的掌握應用級的python編程的工具集。而且這是一項持續不斷的任務。例如,標准庫包含了幾百個模塊,而公開領域提供更多的工具。有可能花個十年甚至更多的時間去研究所有這些工具,尤其是新的工具還在不斷的涌現。一般來說,python提供了一個有層次的工具集:
a)內置工具,和字符串、列表以及字典這些內置類型,會讓編程更迅速。
b)python擴展。可以編寫自己的函數、模塊以及類來擴展python
c)已編譯的擴展。比如使用c或者cpp這樣的外部語言所編寫的模塊進行擴展。
因為python將其工具集分層,可以決定程序任務要多么的深入這個層次:可以讓簡單的腳本使用內置工具,替較大系統新增python所編寫的擴展工具,並且為高級工作編寫編譯好的擴展工具。表35-1總結python程序員可用的內置或現有的功能來源,而有些話題可能會用剩下的python生涯時間來探索:

大型項目的開發工具:PyDoc和文檔字符串;PyChecker和Pylint;PyUnit;doctest;IDE;配置工具(profile模塊);調試器(pdb調試器);發布的選擇(py2exe、PyInstaller以及freeze都可打包字節碼以及python虛擬機,從而成為“凍結二進制”的獨立的可執行文件,也就是不需要目標機器上有安裝python');優化選項(Psyco系統提供實時的編譯器,Shedskin系統提供python對cpp的翻譯器);對於大型項目的提示(23、33、30、15、21、24、17、19章可以深入研究)。
