史上最全Python學習筆記(基於《Python學習手冊(第4版)》)——Part7 異常和工具

Chap32 異常基礎

五種異常處理語句:

  • try/except:捕捉由Python或程序員引起的異常並恢復
  • try/finally:無論異常是否發生,都執行清理行爲
  • raise:手動在代碼中觸發異常
  • assert:有條件地在程序代碼中觸發異常
  • with/as:在Python2.6及後續版本中實現環境管理器

爲什麼使用異常

異常可以在一個步驟內跳至異常處理器,中止開始的所有函數調用進而進入異常管理器。在異常處理器中編寫代碼,來響應在適當時候引發的異常。

異常是一種結構化的“超級goto”。異常處理器會留下標識,並可執行一些代碼。程序前進到某處代碼時,產生異常,因而會使Python立即跳到那個標識,而放棄留下該標識之後所調用的任何激活的函數。這個協議提供了一種固有的方式響應不尋常的事件。再者,因爲Python會立即跳到處理器的語句代碼更簡單——對於可能會發生失敗的函數的每次調用,通常就沒有必要檢查這些函數的狀態碼。

異常的角色

在Python中,異常可以用於各種用途,以下是它最常見的幾種角色。

  • 錯誤處理
  • 事件通知
  • 特殊情況處理
  • 終止行爲
  • 非常規控制流程

異常處理:簡明扼要

默認異常處理器

如果在代碼中沒有可以捕捉某個可能發生的異常,那麼當異常發生時,會一直向上返回到程序頂層,並啓用默認的異常處理器:即打印標準出錯消息(sys.stderr)。這些消息包括引發的異常還有堆棧跟蹤:也就是異常發生時激活的程序行和函數清單。

捕獲異常

在更多的情況下,異常由默認的異常處理器來處理並不是所期望的。例如,服務器程序一般需要在內部錯誤發生時依然保持工作。如果不想要默認的異常行爲,就需要把調用包裝在try語句內,自行捕捉異常。

try:
    fetcher(x,4)
except IndexError:
    print('got exception')

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

def catcher():
    try:
        fetcher(x,4)
    except IndexError:
        print('got exception')
    print('continuing')

引發異常

異常能由Python或程序來引發,也能捕捉或忽略。要手動觸發異常,直接執行raise語句。用戶觸發的異常的捕捉方式和Python引發的異常一樣。

try:
    raise IndexError
except IndexError:
    print("got exception")

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

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

用戶定義的異常

用戶定義的異常能夠通過類編寫,它繼承自一個內置的異常類:通常這個類的名稱叫做Exception。基於類的異常允許腳本建立異常類型、繼承行爲以及附加狀態信息。

class Bad(Exception):
    pass
def demo():
    raise Bad()
try:
    demo()
except Bad:
    print("got Bad")

終止行爲

最後,try語句可以說“finally”,也就是,它可以包含一個finally代碼塊。這看上去就像是異常的except處理器,但是try/finally的組合,可以定義一定會在最後執行時首位的行爲,無論異常是否發生。

try:
    fetcher(x,3)
finally:
    print('after fetch')

Chap33 異常編碼細節

try/except/else語句

try是符合語句,它的最完整形式如下所示。首先是以try爲首行,後面緊跟着(通常)縮進的語句代碼,然後是一個或多個except分句來識別要捕捉的異常,最後是一個可選的else分句。try、except、else這些關鍵字會縮進在相同的層次(也就是垂直對齊)。

try:
    <statement1>  # Run this main action first
except <name1>:
    <statement2>  # Run if name1 is raised during tyr block
except <name2,name3>:
    <statement3>  # Run if any of these exceptions occur
except <name4> as <data>:
    <statement3>  # Run if name4 is raised and get instance raised
except:
    <statement4>  # Run for all (other) exceptions raised
else:
    <statement5>  # Run if no exception was raised during try block

在這個語句中,try首行地下的代碼塊代表此語句的主要動作:試着執行的程序代碼。except子句定義try代碼塊內引發的異常的處理器,而else子句(如果編寫了的話)則是提供沒發生異常時要執行的處理器。在這裏的data元素和raise語句功能有關,之後會進行討論。

以下是try語句的運行方式。當try語句啓動時,Python會標識當前的程序環境,這樣一來,如果異常發生時,才能返回這裏。try首行下的語句就會先執行。接下來會發生什麼事情,則取決於try代碼塊語句執行時是否引發異常。

  • 如果try代碼塊語句執行時的確發生了異常,Python就跳回try,執行第一個符合引發異常的except子句下面的語句。當except代碼塊執行之後(除非except代碼塊引發了另一異常),控制器就會到整個try之後的語句繼續執行。
  • 如果異常發生在try代碼塊中,沒有符合的except子句,異常就會向上傳遞到程序中的之前進入的try中,或者如果它是第一條這樣的語句,就傳遞到這個進程的頂層(這會使Python終止這個程序並打印默認的出錯消息)。
  • 如果try首行底下執行的語句沒有發生異常,Python就會執行else行下的語句(如果有的話),控制器會在整個try語句下繼續。

換句話說,except分句會捕捉try代碼塊執行時發生的任何異常,而else子句只在try代碼塊執行時不發生異常的情況下才會執行。

except子句是專注於異常處理器的:捕捉只在相關try代碼塊中的語句所發生的異常。儘管這樣,因爲try代碼塊語句可以調用卸載程序其他地方的函數,異常的來源可能在try語句自身之外。第35章探討嵌套化時,會再多介紹一些關於這方面的內容。

try語句分句

try語句分句形式:

分句形式 說明
except: 捕捉所有(其他)異常類型
except name: 只捕捉特定的異常
except name,value: 捕捉所列的異常和額外的數據(或實例)
except(name1,name2): 捕捉任何列出的異常
except(name1,name2),value: 捕捉任何列出的異常,並獲取額外的數據
else: 如果沒有異常,就運行
finally: 總是運行此代碼塊

如果try語句塊中引發了異常,Python就會回到try,並搜索第一個和異常名稱相符的except。Python會從頭到尾以及由左至右查看except子句,然後執行第一個相符的except下的語句。如果沒有符合的,異常會向這個try外傳遞。注意:這隻有當action中沒有發生異常時,else纔會執行,當沒有相符except的異常發生時,則不會執行。

空的except子句是一種通用的功能:因爲這是捕捉任何東西,可讓處理器通用化或具體化。再某些場合下,比起列出try中所有可能異常來說,這種形式反而更方便一些。

不過,空except也會引發一些設計的問題:儘管方便,也可能捕捉和代碼無關、意料之外的系統異常,而且可能以外攔截其他處理器的異常。例如,在Python中,即便是系統離開調用,也會觸發異常,而程序員通常會想讓這些事件通過。這一部分末尾會再談這個陷阱。目前而言,要小心使用。

Python3.0引入了一個替代方案來解決這些問題之一——捕獲一個名爲Exception的異常幾乎與一個空的except具有相同的效果,但是,忽略和系統退出相關的異常:

try:
    action()
except Exception:
    ...

這與空的except具有大多相同的便利性,但是,幾乎同樣具有危險性。在下一章學習異常類的時候會介紹這種形式如何發揮其魔力。

try/else分句

如果沒有else,是無法直到控制流程(沒有設置和檢查布爾標誌)是否已經通過try語句,因爲沒有異常引發或者因爲異常發生且被處理過。

try:
    ...run code...
except IndexError:
    ...handle exception...
# Did we get here because the try failed or nor?

就像循環內的else子句讓退出原因更加明顯,else分句也爲try中提供了讓所發生的事情更爲明確而不模糊的語法。

try:
    ...run code...
except IndexError:
    ...handle exception...
else:
    ...no exception occurred...

把else代碼塊中的程序移進try代碼塊中,也幾乎能模擬else分句。不過,這可能會造成不正確的異常分類。如果“沒有異常發生”這個行爲觸發了IndexError,就會視爲try代碼塊的失敗,因此錯誤地觸發try地下的異常處理器(微妙,但真實)改爲使用明確的else分句,可以讓邏輯更爲明確,保證except處理器只會因包裝在try中的代碼真正的失敗而執行,而不是爲else情況中的行爲失敗而執行。

例子:默認行爲

def gobad(x,y):
    return x/y

def gosouth(x):
    print(gobad(x,0))

gosouth(1)  # 程序忽略了觸發的異常,Python會終止這個程序並打印一個消息
---------------------------------------------------------------------------

ZeroDivisionError                         Traceback (most recent call last)

<ipython-input-1-ed4b2a88dc02> in <module>()
      5     print(gobad(x,0))
      6 
----> 7 gosouth(1)


<ipython-input-1-ed4b2a88dc02> in gosouth(x)
      3 
      4 def gosouth(x):
----> 5     print(gobad(x,0))
      6 
      7 gosouth(1)


<ipython-input-1-ed4b2a88dc02> in gobad(x, y)
      1 def gobad(x,y):
----> 2     return x/y
      3 
      4 def gosouth(x):
      5     print(gobad(x,0))


ZeroDivisionError: division by zero
try:
    gosouth(1)
except ZeroDivisionError:
    print('除數不能爲零!')
除數不能爲零!

例子:捕捉內置異常

如果不想在Python引發異常事時造成程序終止,只要把程序邏輯包裝在try中進行捕捉就行了。這是網絡服務器這類程序很重要的功能,因它們必須不斷持續運行下去。

def kaboom(x,y):
    print(x+y)
    
try:
    kaboom([0,1,2],"spam")
except TypeError:
    print('Hello world!')
print('resuming here')
Hello world!
resuming here

注意:一旦捕捉了錯誤,控制權就會在捕捉的地方繼續下去(也就是在try之後),沒有直接的方式可以回到異常發生的地方(在這裏,就是函數kaboom中)。總之,這會讓異常更像是簡單的跳躍,而不是函數調用:沒有辦法回到觸發錯誤的代碼。

try/finally語句

如果try語句中包含了finally子句,Python一定會在try語句後執行其語句代碼塊,無論try代碼塊執行時是否發送了異常。其一般形式如下所示:

try:
    <statements>
finally:
    <statements>

當想確定某些程序代碼執行後,無論程序的異常行爲如何,有個動作一定會發生,那麼這種形式就很有用。在實際應用中,這可以讓你定義一定會發生的清理動作,例如,文件關閉以及服務器斷開連接等。

例子:利用try/finally編寫終止行爲

class MyError(Exception):pass

def stuff(file):
    raise MyError()
    
file=open('test.txt','w')
try:
    stuff(file)
finally:
    file.close()
    print('not reached')
not reached



---------------------------------------------------------------------------

MyError                                   Traceback (most recent call last)

<ipython-input-3-1f466edc3fb1> in <module>()
      6 file=open('test.txt','w')
      7 try:
----> 8     stuff(file)
      9 finally:
     10     file.close()


<ipython-input-3-1f466edc3fb1> in stuff(file)
      2 
      3 def stuff(file):
----> 4     raise MyError()
      5 
      6 file=open('test.txt','w')


MyError: 

統一try/except/finally語句

如今,可以在同一個try語句中混合finally、except以及else子句。

try:
    main-action
except Exception1:
    handler1
except Exception2:
    handler2
...
else:
    else-block
finally:
    finally-block

這個語句中的main-action代碼塊會先執行。如果該程序代碼引發異常,那麼所有except代碼塊都會逐一測試,尋找與拋出的異常相符的語句。如果一你發的異常是Exception1,則會執行handler1代碼塊;如果引發的是Exception2,則執行handler2代碼塊;以此類推。如果美歐引發異常,則會執行else-block代碼塊。無論發生了什麼,總是會執行finally-block代碼塊。

統一try語句語法

當像這樣的組合的時候,try語句必須有一個except或一個finally,並且其部分的順序必須如下所示:try->except->else->finally

其中,else和finally是可選的,可能會有0或多個except,但是,如果出現一個else,則必須至少一個except。實際上,該try語句包含兩個部分,帶有一個可選的else的except,以及(或)finally。

實際上,下面的方式更準確地描述了這一組合的語句語法形式(方括號表示可選,星號表示0或多個):

try:  # Format1
    statements
except [type [as value]]:
    statements
[except [type [as value]]:
    statements]*
[else:
    statements]
[finally:
    statements]
    
try:  # Format2
    statements
finally:
    statements

合併try的例子

以下示範了合併的try語句的執行情況。

sep='-'*32+'\n'
print(sep+'EXCEPTION RAISED AND CAUGHT')

try:
    x='spam'[99]
except IndexError:
    print('except run')
finally:
    print('finally run')
print('after run')

print(sep+'NO EXCEPTION RAISED')

try:
    x='spam'[3]
except IndexError:
    print('except run')
finally:
    print('finally run')
print('after run')

print(sep+'NO EXCEPTION RAISED,WITH ELSE')

try:
    x='spam'[3]
except IndexError:
    print('except run')
else:
    print('else run')
finally:
    print('finally run')
print('after run')

print(sep+'EXCEPTION RAISED BUT NOT CAUGHT')
try:
    x=1/0
except IndexError:
    print('except run')
finally:
    print('finally run')
print('after run')
--------------------------------
EXCEPTION RAISED AND CAUGHT
except run
finally run
after run
--------------------------------
NO EXCEPTION RAISED
finally run
after run
--------------------------------
NO EXCEPTION RAISED,WITH ELSE
else run
finally run
after run
--------------------------------
EXCEPTION RAISED BUT NOT CAUGHT
finally run



---------------------------------------------------------------------------

ZeroDivisionError                         Traceback (most recent call last)

<ipython-input-4-0adc33ae6147> in <module>()
     34 print(sep+'EXCEPTION RAISED BUT NOT CAUGHT')
     35 try:
---> 36     x=1/0
     37 except IndexError:
     38     print('except run')


ZeroDivisionError: division by zero

raise語句

raise語句可以顯式地觸發異常,其組成是:raise關鍵字,後面跟着可選的要引發的類或一個類的實例:

raise <instance> # Raise instance of class
raise <class>  # Make and raise instance of class
raise  # Reraise the most recent exception

第一個raise形式是最常見的,直接提供一個實例,要麼是在raise之前創建的,要麼是在raise語句中自帶的。如果傳遞一個類,Python調用不帶構造函數參數的類,以創建被引發的一個實例;這個格式等同於在類引用後面添加圓括號。最後的形式重新引發最近引發的異常;它通常用於異常處理器中,以傳播已經捕獲的異常。

對於內置異常,如下兩種形式是對等的,都會引發指定的異常類的一個實例,但是第一種形式隱式地創建實例:

raise IndexError
raise IndexError()

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

exc=IndexError()
raise exc

excs=[IndexError,TypeError]
raise excs[0]

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

try:
    ...
except IndexError as X:
    ...

as在try處理器中是可選的(如果忽略它,該實例直接不會分配給一個名稱),但是,包含它將使得處理器能夠訪問實例中的數據以及異常類中的方法。

這種模式對於用類編寫的用戶定義的異常類也同樣有效——例如,如下的代碼,傳遞異常類構造函數參數,該參數通過分配的實例在處理器中變得可用:

class MyExc(Exception):pass
...
raise MyExc('spam')
...
try:
    ...
except MyExc as X:
    print(X.args)

不管如何指定異常,異常總是通過實力對象來識別,並且大多數時候在任意給定的時刻激活。一旦異常在程序中某處由一條except子句捕獲,它就死掉了(例如,不會傳遞到另一個try),除非由另一個raise語句或錯誤重新引發它。

利用raise傳遞異常

raise語句不包括異常名稱或額外數據值時,就是重新引發當前異常。如果需要捕捉和處理一個異常,又不希望異常在程序代碼中死掉時,一般就會使用這種形式。

try:
    raise IndexError('spam')
except IndexError:
    print('propagating')
    raise
propagating



---------------------------------------------------------------------------

IndexError                                Traceback (most recent call last)

<ipython-input-5-30f65ef43030> in <module>()
      1 try:
----> 2     raise IndexError('spam')
      3 except IndexError:
      4     print('propagating')
      5     raise


IndexError: spam

通過這種方式執行raise時,會重新引發異常,並將其傳遞給更高層的處理器(或者頂層的默認處理器,它會停止程序,打印出標準出錯消息)。注意傳遞給異常類的參數是如何出現在出錯消息中的,將會在下一章中瞭解爲什麼會這樣。

Python3.0異常鏈:raise from

Python3.0也允許raise語句擁有一個可選的from子句:

raise exception from otherexception

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

try:
    1/0
except Exception as E:
    raise TypeError('Bad!') from E
---------------------------------------------------------------------------

ZeroDivisionError                         Traceback (most recent call last)

<ipython-input-6-145730a4b1e7> in <module>()
      1 try:
----> 2     1/0
      3 except Exception as E:


ZeroDivisionError: division by zero


The above exception was the direct cause of the following exception:


TypeError                                 Traceback (most recent call last)

<ipython-input-6-145730a4b1e7> in <module>()
      2     1/0
      3 except Exception as E:
----> 4     raise TypeError('Bad!') from E


TypeError: Bad!

當在一個異常處理器內部引發一個異常的時候,隱式地遵從類似的過程:前一個異常附加到新的異常的__context__屬性,並且如果該異常未捕捉的話,再次顯示在標準出錯消息中。這是一個高級的並且多少還有些含糊的擴展,因此,請參閱Python的手冊以瞭解詳細內容。

assert語句

Python還包括了assert語句,這種情況有些特殊。這是raise常見使用模式的語法簡寫,assert可視爲條件式的raise語句。該語句的形式爲:

assert <test>,<data>

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

if __debug__:
    if not <test>:
        raise AssertionError(<data>)

換句話說,如果raise計算爲假,Python就會引發異常:data項(如果提供了的話)是異常的額外數據。就像所有異常,引發的AssertionError異常如果沒被try捕捉,就會終止程序,在此情況下數據項將作爲出錯消息的一部分展示。

assert語句是附加的功能,如果使用-O Python命令行標誌位,就會從程序編譯後的字節碼中移除,從而優化程序。AssertionError是內置異常,而__debug__標誌位是內置變量名,除非有使用-O標誌,否則自動設置爲1(真值)。使用類似Python -O main.py的一個命令行來在優化模式中運行,並且關閉assert。

例子:收集約束條件(但不是錯誤)

Assert語句通常是用於驗證開發期間程序狀況的。顯示時,其出錯消息正文會自動包括源代碼的行信息,以及列在assert語句中的值。

def f(x):
    assert x<0,'x must be negative'
    return x**2

try:
    x=f(1)
finally:
    print(x)
m



---------------------------------------------------------------------------

AssertionError                            Traceback (most recent call last)

<ipython-input-11-fcecd8df4135> in <module>()
      4 
      5 try:
----> 6     x=f(1)
      7 finally:
      8     print(x)


<ipython-input-11-fcecd8df4135> in f(x)
      1 def f(x):
----> 2     assert x<0,'x must be negative'
      3     return x**2
      4 
      5 try:


AssertionError: x must be negative

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

這類assert一般都是多餘的:因爲Python會在預見錯誤時自動引發異常,讓Python替你把事情做好了就行了。另一個assert常見用法例子,可以參考第28章的抽象超類例子。在那裏,使用assert讓未定義的調用失敗並打印消息。

with/as環境管理器

簡而言之,with/as語句的設計是作爲常見try/finally用法模式的替代方案。就像try/finally語句,with/as語句也是用於定義必須執行的終止或“清理”行爲,無論處理步驟中是否發送異常。不過,和try/finally不同的是,with語句支持更豐富的基於對象的協議,可以爲代碼塊定義支持進入和離開的動作。

Python以還擊管理器強化一些內置工具,例如,自動自行關閉的文件,以及對鎖的自動上鎖和開鎖,程序員也可以用類編寫自己的環境管理器。

基本使用

with語句的基本格式如下:

with expression [as value]:
    with-block

在這裏,exprission要返回一個對象,從而支持環境管理協議(稍後會談到這個協議的更多內容)。如果選用的as子句存在時,此對象也可以返回一個值,賦值給變量名value。

注意:value並非賦值爲expression的結果。expression的結果是支持環境協議的對象,而value則是賦值爲其他的東西。然後,expression返回的對象可在with-block開始前,先執行啓動程序,並且在該代碼塊完成後,執行終止程序代碼,無論該代碼塊是否引發異常。

有些內置的Python對象已經得到強化,支持環境管理協議,因此可以用於with語句。例如,文件對象有環境管理器,可以在with代碼塊後自動關閉文件,無論是否引發異常:

with open(r'C:\misc\data') as myfile:
    for line in myfile:
        print(line)
        ...more code here...

在這裏,對open的調用,會返回一個簡單文件對象,賦值給變量名myfile。可以用一般的文件工具來使用myfile:就此而言,文件迭代器會在for循環內逐行讀取。

然而,此對象也支持with語句所使用的環境管理協議。在這個with語句執行後,環境管理機制保證由myfile所引用的文件對象會自動關閉,即使該處理文件時,for循環引發了異常也是如此。

儘管文件對象在垃圾回收時自動關閉,然而,並不總是能夠容易地知道會何時發送。with語句的這種用法作爲一種替代,允許程序員確定在一個特定代碼塊執行完畢後會發生關閉。正如前面所看到的,可以使用更通用而明確的try/finally語句來實現類似的效果,但是,這裏需要4行代碼而不是1行:

myfile=open(r'C:\misc\data')
try:
    for line in myfile:
        print(line)
        ... more code here...
finally:
    myfile.close()

這個教程不會討論Python的多線程模塊,但是那些模塊所定義的鎖和條件變量同步對象也可以和with語句一起使用,因爲他們支持環境管理協議。

lock=threading.Lock()
with lock:
    # critical section of code
    ... access shared resources...

在這裏,環境管理機制保證鎖會在代碼塊執行前自動獲得,並且一旦代碼塊完成就釋放,而不管異常輸出是什麼。

decimal模塊(小數模塊)也使用環境管理器來簡化存儲和保存當前小數配置環境(定義了賦值計算時的精度和取整的方式)。

with decimal.localcontext() as ctx:
    ctx.prec=2
    x=decimal.Decimal('1.00')/decimal.Decimal('3.00')

這條語句運行後,當前線程的環境管理器狀態自動恢復到語句開始之前的狀態。要使用try/finally做到同樣的事,需要提取保存環境並手動恢復它。

環境管理協議

儘管一些內置類型帶有環境管理器,程序員還是額可以自己編寫一個。要實現環境管理器,使用特殊的方法來接入with語句,該方法屬於運算符重載的範疇。用在with語句對象中所需要的接口有點複雜,而多數程序員只需知道如何使用現有的還擊管理器。

以下是with語句實際的工作方式:

  • 計算表達式,所得到的對象成爲環境管理器,它必須有__enter__和__exit__方法。
  • 環境管理器的__enter__方法會被調用。如果as子句存在,其返回值會賦值給as子句中的變量,否則,直接丟棄。
  • 代碼塊中嵌套的代碼會執行。
  • 如果with代碼塊引發異常,__exit__(type,value,traceback)方法就會被調用(帶有有異常細節)。這些也是由sys.exc_info返回的相同值。如果此方法返回值爲假,則異常會重新引發。否則,異常會終止。正常情況下異常是應該被重新引發,這樣的話才能傳遞到with語句之外。
  • 如果with代碼塊沒有引發異常,__exit__方法依然會被調用,其type、value以及traceback參數會以None傳遞。

來看一個示範,以下定義一個環境管理器對象,跟蹤其所用的任意一個with語句內with代碼塊的進入和退出。

class TraceBlock:
    def message(self,arg):
        print('running',arg)
    def __enter__(self):
        print('starting with block')
        return self
    def __exit__(self,exc_type,exc_value,exc_tb):
        if exc_type is None:
            print('exited normally\n')
        else:
            print('raise an exceptino!',exc_type)
            return False
with TraceBlock() as action:
    action.message('test 1')
    print('reached')

with TraceBlock() as action:
    action.message('test 2')
    raise TypeError
    print('not reached')
starting with block
running test 1
reached
exited normally

starting with block
running test 2
raise an exceptino! <class 'TypeError'>



---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-14-16d218488674> in <module>()
     17 with TraceBlock() as action:
     18     action.message('test 2')
---> 19     raise TypeError
     20     print('not reached')


TypeError: 

Chap34 異常對象

基於類的異常有如下特點

  • **提供類型分類,對今後的修改有更好的支持。**以後增加新異常時,通常不需要在try語句中進行修改。
  • **它們附加了狀態信息。**異常類提供了存儲在try處理器中所使用的環境信息的合理地點:這樣的話,可以擁有狀態信息以及可調用的方法,並且可以通過實例進行讀取。
  • **它們支持繼承。**基於類的異常允許參與繼承層次,從而可以獲得並定製共同的行爲。例如,繼承的顯示方法可以提供通用的出錯信息的外觀。

異常:回到未來

基於類的異常

類可以讓程序員組織的異常分類,使用和維護起來更靈活。其次,類可以附加異常的細節,而且支持繼承。

類異常是由超類關係進行匹配的:只要except子句列舉了異常的類或其任何超類名,引發的異常就會匹配該子句。

也就是說,當try語句的except子句列出一個超類時,就可以捕捉該超類的實例,以及類樹中所有較低位置的子類的實例。結果就是,類異常支持異常層次的架構:超類變成分類的名稱,而子類變成這個分類中特定種類的異常。except子句列出一個通用的異常超類,就可捕捉整個分類中的各種異常:任何特定的子類都可以匹配。

類異常例子

以下的代碼例子中,定義了一個名爲general的超類,以及兩個子類Specific1和Specific2。這個例子說明異常分類的概念:General是分類的名稱,而其兩個子列Specific1和Specific2是這個分裂中特定種類的異常。捕捉General的處理器也會捕捉其任何子類,包括Specific1和Specific2。

class General(Exception):pass
class Specific1(General):pass
class Specific2(General):pass

def raiser0():
    X=General()
    raise X
    
def raiser1():
    X=Specific1()
    raise X

def raiser2():
    X=Specific2()
    raise X

for func in (raiser0,raiser1,raiser2):
    try:
        func()
    except General:
        import sys
        print('caught:',sys.exc_info()[0])
caught: <class '__main__.General'>
caught: <class '__main__.Specific1'>
caught: <class '__main__.Specific2'>

以上代碼十分直接,但這裏有一些實現細節需要注意:

Exception超類

用來構建異常分類樹的類擁有很少的需求——在這裏,且通常,其主體不做任何事情而直接通過。注意,這裏頂層的類是如何從內置的Exception類繼承的,這在PY3中式必須的。由於Exception提供了一些有用的行爲,在隨後纔會遇到這些行爲,一次你這裏不使用它們。但在任何Py版本中,從它那裏繼承是個好主意。

引發實例

在類異常模式中,總是引發和捕獲一個類的實例對象。如果在一個raise中列出了類名而沒有圓括號,那麼Python調用該類而沒有構造函數參數來產生一個實例。異常實例可以在該raise之前創建,就像這裏所做的一樣,或者在raise語句自身中創建。

捕獲分類

這段代碼也包含一些函數,引發三個類實例使其成爲異常,此外,有個頂層try會調用安歇函數,並捕捉General異常(同一個try也會捕捉兩個特定的異常,因爲它們是General的子類)。

異常細節

下一章會再談到這所用到的異常處理器sys.exc_info掉哦用:這是一種抓取最近發生異常的常用方式。簡而言之,對基於類的異常而言,其結果中的第一個元素就是引發異常類,而第二個是實際引發的實例。這裏的except子句捕獲了一個分類中的所有類,在這樣的一條通用的except子句中,sys.exc_info是決定到底發生了什麼的一種方式。在這一特別情況下,它等價於獲取實例的__class__屬性。

正如在下一章將看到的,sys.exc_info方法通常也與捕獲所有內容的空的except子句一起使用。最後一點值得進一步說明。當捕獲了一個異常,就可以確定該實例是except種列出的類的一個實例,或者是其更具體的子類中的一個。因此,實例的__class__屬性也給出了異常類型。例如,如下的變體和前面的例子起着同樣的作用。

class General(Exception):pass
class Specific1(General):pass
class Specific2(General):pass

def raiser0():raise General()
def raiser1():raise Specific1()
def raiser2():raise Specific2()
    
for func in (raiser0,raiser1,raiser2):
    try:
        func()
    except General as X:
        print('caught:',X.__class__)
caught: <class '__main__.General'>
caught: <class '__main__.Specific1'>
caught: <class '__main__.Specific2'>

由於__class__可以像這樣使用來決定引發的異常的具體類型,因此sys.exc_info對於空的except子句更有用,否則的話,沒有一種方式來訪問實例及其類。此外,更實用的程序通常根本不必關注引發了哪個具體的異常——通過一般調用實例的方法,自動把修改後的行爲分派給引發的異常。

爲什麼使用異常類

對大型或多層次的異常而言,在一個except子句中使用類捕捉分類,會比列出一個分類中的每個成員更爲簡單。因此,可以新增子類擴展異常層次,而不會破壞現有的代碼。

把自己編寫的庫中的異常安排到一個類樹中,讓一個共同的超類來包含整個類型,如此,用戶只需列出共同的超類(也就是分類),來捕捉庫的所有異常,無論現在還是以後。這樣在未來修改代碼時,作爲共同超類的新的子類就能夠增加新的異常。

結果就是用戶代碼捕捉庫的異常依然保持正常工作,沒有改變。事實上,可以在未來任意新增、刪除以及修改異常,只要客戶端使用的是超類的名稱,就和異常幾種的修改無關。換句話說,對於維護的問題來說,類異常提供了更好的答案。

內置Exception類

在Python3中,所有熟悉的異常(例如,SyntaxError)其實都是預定義的類,可以作爲內置變量名,可以作爲builtin模塊中的內置名稱使用。此外,Python把內置異常組織成層次,來支持各種捕捉模式。

BaseException

異常的頂級根類。這個類不能當作是由用戶定義的類直接繼承的(而是要使用Exception)。它提供了子類所繼承的默認的打印和狀態保持行爲。如果在這個類的一個實力上調用str內置函數(例如,通過print),該類返回創建實例的時候所傳遞的構造函數參數的顯示字符串(如果沒有的話,是一個空字符串)。此外,除非子類替代了這個類的構造函數,在實例構造時候傳遞給這個類的所有參數都將作爲一個元組存儲在其args屬性中。

Exception

與應用相關的異常的頂層根超類。這是BaseException的一個直接子類,並且是其他所有內置異常的超類,除了系統退出事件(SystemExxit、KeybordInterrupt和GeneratorExit)之外。幾乎所有的用戶定義的類都應該繼承自這個類,而不是BaseException。當遵從這一慣例的時候,在一條try語句的處理器中指明Excetpion,會確保程序將捕獲除了系統退出事件之外的所有異常,通常該事件是允許通過的。實際上,Exception變成了try語句中的一個全捕獲,並且比一條空的except更精確。

ArithmeticError

所有數值錯誤的超類,並且這是Exception的一個子類。

OverflowError

識別特定的數值錯誤的子類。

其他的內置異常類,可以在Python Pocket Reference或Python手冊這樣的幫助文本中進一步閱讀關於這個結構的內容。

內置異常分類

內置類樹可以讓程序員選擇處理器具體或通用的程度。選擇某個特定的內置異常分類(比如ArithmeticError),在try中列出它,則將只會捕獲所引發的這個異常分類下的所有異常,而不能捕捉其他異常。

與之相似的是,因爲Exception是Python中所有應用程序級別的異常的超類,通常可以將其作爲一個全捕獲,其效果與一條空的except很相似,但它語序系統退出異常而像平常那樣通過。

try:
    action()
except Exception:
    ...handle all opplication exceptions...
else:
    ...handle no-exception case...

當然這一技術在Python3中不會更爲可靠,因爲它要求所有的類都派生自內置異常。即便在PY3中,這種方案會像空的except一樣遭遇到大多數相同的潛在陷阱,就像之前一張所介紹的那樣——它可能攔截用於其他地方的異常,並且可能掩蓋了真正的編程錯誤。因此在下一章的“陷阱”部分將會回顧。

默認打印和狀態

內置異常還提供了默認打印顯示和狀態保持,它往往和用戶定義的類所需的邏輯一樣的多。除非重新定義了類繼承自它們的構造函數,傳遞給這些類的任何構造函數參數都會保存在實例的args元組屬性中,並且當打印該實例的時候自動顯示。這說明了爲什麼傳遞給內置異常類的參數會出現在出錯消息中,當打印實例的時候,附加給實例的任何構造函數參數就會顯示:

raise IndexError
---------------------------------------------------------------------------

IndexError                                Traceback (most recent call last)

<ipython-input-3-55a00e7db5b5> in <module>()
----> 1 raise IndexError


IndexError: 
raise IndexError('spam')
---------------------------------------------------------------------------

IndexError                                Traceback (most recent call last)

<ipython-input-4-53b7e456dbfa> in <module>()
----> 1 raise IndexError('spam')


IndexError: spam
I=IndexError('spam')
I.args
('spam',)

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

class E(Exception):pass

try:
    raise E('spam')
except E as X:
    print(X,X.args  # Displays and saves constructor arguments
spam ('spam',)

注意,該異常實例對象並非字符串本身,但是當打印的時候,使用29章處介紹的__str__運算符重載協議來提供顯示字符串;要連接真正的字符串,執行手動轉化:str(X)+‘string’。

儘管這種自動轉檯和實現支持本身是有用的,但對於特定的顯示和狀態保持需求,總是可以重新定義Exception子類中的__str__和__init__這一的繼承方法。下一小節介紹如何做到這一點。

定製打印顯示

默認情況下,捕獲並打印基於類的實例的時候,它們會顯示編程時傳遞給類構造函數的任何內容:

class MyBad(Exception):pass

try:
    raise MyBad('Sorry--my mistake!')
except MyBad as X:
    print(X)
Sorry--my mistake!

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

raise MyBad('Sorry--my mistake!')
---------------------------------------------------------------------------

MyBad                                     Traceback (most recent call last)

<ipython-input-8-264d8a9614b8> in <module>()
----> 1 raise MyBad('Sorry--my mistake!')


MyBad: Sorry--my mistake!

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

class MyBad(Exception):
    def __str__(self):
        return 'Always look on the bright side of life...'
    
try:
    raise MyBad()
except MyBad as X:
    print(X)
Always look on the bright side of life...
raise MyBad()
---------------------------------------------------------------------------

MyBad                                     Traceback (most recent call last)

<ipython-input-10-ff3b7d17d944> in <module>()
----> 1 raise MyBad()


MyBad: Always look on the bright side of life...

這裏需要注意的細微一點是,通常要爲此目的而重新定義__str__,因爲內置的超類已經有一個__str__方法,並且在大多數環境下(包括打印),__str__優先於__repr__。如果定義了一個__repr__,打印將會很樂意地調用超類的__str__。

對於未捕獲的異常,方法返回的內容都包含在出錯消息中,並且打印異常的時候顯式化。這裏,方法返回一個硬編碼的字符串來說明,但是,它也可以執行任意的文本處理,可能附加到實例對象的狀態信息。下一小節將介紹狀態信息選項。

定製數據和行爲

除了支持靈活的層級,異常類還提供了把額外狀態信息存儲爲實例屬性的功能。

提供異常細節

當引發一個異常的時候,可能會跨越任意的文件界限——觸發異常的raise語句和捕獲異常的try語句可能位於完全不同的模塊文件中。在一個全局變量中存儲額外的細節通常是不可行的,因爲try語句可能不知道全局變量位於哪個文件中。在異常自身中傳遞額外的狀態信息,這允許try語句更可靠地訪問它。

使用類,這幾乎是自動化的。當引發一個異常的時候,Python隨着異常傳遞類實例對象。在try語句中的代碼,可以通過在一個except處理器中的as關鍵字之後列出一個額外的變量,來訪問引發的異常。這提供了一個自然的鉤子,以用來爲處理器提供數據和行爲。

例如,解析數據文件的一個程序可能通過引發一個異常實例來表示格式化錯誤,而該實例用關於錯誤的額外細節來填充:

class FormatError(Exception):
    def __init__(self,line,file):
        self.line=line
        self.file=file

def parser():
    raise FormatError(42,file='test.txt') # When error found

try:
    parser()
except FormatError as X:
    print('Error at',X.file,X.line)
Error at test.txt 42

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

class FormatError(Exception):pass

def parser():
    raise FormatError(42,'test.txt')
    
try:
    parser()
except FormatError as X:
    print('Error at',X.args[1],X.args[0])
Error at test.txt 42

提供異常方法

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

class FormatError(Exception):
    logfile='formaterror.txt'
    def __init__(self,line,file):
        self.line=line
        self.file=file
    def logerror(self):
        log=open(self.logfile,'a')
        print('Error at',self.file,self.line,file=log)

def parser():
    raise FormatError(40,'test.txt')
    
try:
    parser()
except FormatError as exc:
    exc.logerror()

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

with open('formaterror.txt','r') as f :
    for line in f.readlines():
        print(line)
Error at test.txt 40

在這樣的一個類中,方法也可能繼承自超類,並且實例屬性提供了一個地方來保存狀態信息,狀態信息提供了額外環境用於隨後的方法調用。此外,異常類可以自由地定製和擴展繼承的行爲。換句話說,由於它們是用類定義的,所以在第六部分中所學習的所有關於OOP的好處,對於Python中的異常來說的都是可用的。

Chap35 異常的設計

嵌套異常處理器

到目前爲止,所舉的例子都只使用了單一的try語句來捕捉異常,但如果try中還有try,那會發生什麼事呢?就此而言,如果try調用一個會執行另一個try的函數,這代表了什麼意思呢?從技術角度上講,從語法和代碼運行時的控制流程來看,try語句是可以嵌套的。

如果知道Python會在運行時講try語句放入堆棧,這兩種情況就可以理解了。當發生異常時,Python會回到最近進入、具有相符except分句的try語句。因爲每個try語句都會留下標識,Python可檢查堆棧的標識,從而跳回較早的try。這種處理器的嵌套化,就是所談到的異常向上傳遞至較高的處理器的意思:這類處理器就是在程序執行流程中較早進入的try語句。

進入try代碼塊的代碼量可能很大(例如,它可能包含了函數調用),而且通常會啓用正在監視相同異常的其他代碼。當異常最終引發時,Python會跳回到匹配該異常、最近進入的try語句,執行該語句的except分句,然後在try語句後繼續下去。

一旦異常被捕捉,其生命就結束:控制權不會跳回到所有匹配這個異常、相符的try語句;只有第一個try有機會對它進行處理。與之相對比的是,當try/finally語句嵌套且發生異常時,每個finally代碼塊都會執行:Python會持續把異常往上傳遞到其他try語句上,而最終可能達到頂層默認處理器(標準出錯消息打印器)。finally子句不會終止異常,而是指明異常傳播過程中,離開每個try語句之前要執行的代碼。如果異常發生時,有很多try/finally都在活動,它們就都會運行,除非有個try/except在這個過程中捕捉某處該異常。

換句話說,引發異常時,程序去向何方完全取決於異常在何處發生:這是腳本運行時控制流程的函數,而不僅僅是其語法。異常的傳遞,基本上就是回到處理先前進入但尚未離開的try。只要控制權碰到相符except子句,傳遞就會停止,而通過finally子句時就不會。

例子:控制流程嵌套

分析一個例子,讓這個概念更爲具體。以下代碼定義了兩個函數,action2是寫成要觸發異常(做數字和序列的加法),而action1把action2調用封裝在try處理器內,以捕捉異常。

def action2():
    print(1+[])
def action1():
    try:
        action2()
    except TypeError:
        print('inner try')
try:
    action1()
except TypeError:
    print('outer try')
inner try

可以看到,代碼底端的頂層模塊代碼,也在try處理器中包裝了action1調用。當action2觸發TypeError異常時,就有兩個激活的try語句:其中一個在action1中,另一個在代碼底端。Python會挑選並執行具有相符的except、最近的try,而在這個例子中就是action1中的try。

這就正如前邊提到的,異常最後所在之處,取決於程序運行時的控制流程。因此,想要知道要去哪裏,就需要知道在哪裏。就這個例子而言,異常在哪裏進行處理是控制流程的函數,而不是語句的語法。然而,也可以用語法把異常處理器嵌套化——等一下會看到與其等效的情況。

例子:語法嵌套化

從語法上有可能讓try語句通過其源代碼中的位置來實現嵌套。

try:
    try:
        action2()
    except TypeError: # Most recent matching try
        print('inner try')
except TypeError: # Here,only if nested handler re-raises
    print('outer try')

上面的這段代碼只是像之前的那個例子一樣(行爲也相同),設置了相同的處理器嵌套結構。實際上,語法嵌套的工作就如一開始描述的那樣。唯一的差別就在於,嵌套處理器實際上是嵌入try代碼塊中,而不是卸載其他被調用的函數中。例如,嵌套的finally處理器會因一個異常而全部啓動,無論是語法上的嵌套,或者因運行時流程經過代碼中某個部分。

try:
    try:
        raise IndexError
    finally:
        print('spam')
finally:
    print('SPAM')
spam
SPAM



---------------------------------------------------------------------------

IndexError                                Traceback (most recent call last)

<ipython-input-18-5eee2ecdf687> in <module>()
      1 try:
      2     try:
----> 3         raise IndexError
      4     finally:
      5         print('spam')


IndexError: 

有關語法嵌套更有有的例子,可以考慮下面的代碼

def raise1():raise IndexError
def noraise():return None
def raise2(): raise SyntaxError
    
for func in (raise1,noraise,raiser2):
    print('\n',func,sep='')
    try:
        try:
            func()
        except IndexError:
            print('caught IndexError')
    finally:
        print('finally run')
<function raise1 at 0x000002B07E0AD730>
caught IndexError
finally run

<function noraise at 0x000002B07E0AD158>
finally run

<function raiser2 at 0x000002B07DFBA598>
finally run



---------------------------------------------------------------------------

Specific2                                 Traceback (most recent call last)

<ipython-input-19-061748005958> in <module>()
      7     try:
      8         try:
----> 9             func()
     10         except IndexError:
     11             print('caught IndexError')


<ipython-input-2-772a6fee7a08> in raiser2()
      5 def raiser0():raise General()
      6 def raiser1():raise Specific1()
----> 7 def raiser2():raise Specific2()
      8 
      9 for func in (raiser0,raiser1,raiser2):


Specific2: 

此代碼在異常引發時,會對其進行捕捉,而且無論是否發生異常,都會執行finally終止動作。這需要一點時間去理解,但是其效果會很像在單個try語句內結合except和finally。

異常的習慣用法

異常不總是錯誤

Python中,所有錯誤都是異常,但不是所有異常都是錯誤。例如,input函數在每次調用時,都會在文件末尾時引發內置的EOFError。和文件方法不同的是,這個函數並不返回空字符串:input的空字符串是指空行。除了EOFError的名稱,這個異常在這種環境下也只是信號而已,不是錯誤。因爲有這種行爲,除非文檔末尾應該終止腳本,否額,input通常會出現在try處理器內,並嵌入循環內,如下面的代碼所示:

while True:
    try:
        line=input()
    except EOFError:
        break
    else:
        ...process next line here...

其他內置異常都是類似的信號,而不是錯誤——例如,調用sys.exit()並在鍵盤上按下Ctrl-C,會分別引發SystemExit和KeyboardInterrupt。Python也有一組內置異常,代表警告,而不是錯誤。其中有些代表了正在使用不推薦的(即將退出的)語言功能的信號。

函數信號條件和raise

用戶定義的異常也可以引發非錯誤的情況。例如,搜索程序可以寫成找到相符者時引發異常,而不是爲調用者返回狀態標誌來攔截。下面的代碼中,try/except/else處理器做的就是if/else返回值的測試工作。

class Found(Exception):pass

def searcher():
    if ...success...:
        raise Found()
    else:
        return None

try:
    searcher()
except Found:
    ...success...
else:
    ...failure...

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

class Failure(Exception):pass

def searcher():
    if ...success...:
        return ...founditem...
    else:
        raise Failure()
        
try:
    item=searcher()
except Failure:
    ...report...
else:
    ...use item here...

因爲Python核心是動態類型多態的,所以通常更傾向於使用異常來發出這類情況的信號,而不是警示性的返回值。

關閉文件和服務器連接

作爲概括,異常處理工具通常也用來確保系統資源中介,不管在處理過程中是否發生錯誤。

例如,一些服務器需要關閉連接,從而終止一個會話。與之類似,輸出文件可能需要關閉把緩衝區寫入磁盤的調用,並且,如果沒有關閉輸入文件的化,它可能佔用文件描述符;儘管在垃圾收集的時候,如果文件對象還打開它會自動關閉,但這有時候很難確保。

確保一個特殊代碼塊的終止操作的更通用和顯式的方式是try/finally語句:

myfile=open(r'C:\misc\script','w')
try:
    ...process myfile...
finally:
    myfile.close()

正如在第33章看到的那樣,還可以使用環境管理器,從而自動終止或關閉對象。

而二者選誰,則通常取決於程序。與try/finally相比,環境管理器更爲隱式,它與Python通常的設計哲學背道而馳。環境管理器可定也不太常見,它們通常只對選定的對象可用,並且編寫用戶定義的環境管理器來處理通用的種植需求,比編寫要給try/finally更爲複雜。

另一方面,使用已有的環境管理器,比使用try/finally需要更少的代碼,如前面的例子所示。此外,環境管理器協議除了支持推出動作,還支持進入動作。儘管try/finally可能是更加廣爲應用的技術,環境管理器可能更適合於可以使用它們的地方,或者可以允許它們的額外複雜性的地方。

在try外進行調試

也可以利用異常處理器,取代Python的默認頂層異常處理行爲。在頂層代碼中的外層try中包裝整個程序(或對它調用),就可以捕捉任何程序執行時會發生的異常,因此可破壞默認的程序終止行爲。

下面的代碼中,空的except子句會捕捉任何程序執行時所引發的而違背捕捉到的異常。要去的所發生的實際異常,可以從內置sys模塊取出sys.exc_info函數的調用結果。這回返回一個u安祖,而元組之前兩個元素會自動包含當前異常的類和引發的實例對象(關於sys.exc_info的更多內容稍後介紹)。

try:
    ...run program...
except:
    import sys
    print('uncought!',sys.exc_info()[0],sys.exc_info()[1])

這種結構在開發期間會經常使用,在錯誤發生後,仍保持程序處於激活狀態:這樣可以執行其他測試,而不用重新開始。測試其他程序時,也會用到它,就像下一屆所描述的那樣。

運行進程中的測試

可以在測試驅動程序的應用中結合剛纔所見到的一些編碼模式,在同一進程中測試其他代碼。

import sys

log=open('testlog','a')
from testapi import moreTests,runNextTest,testName

def testdrive():
    while moreTests():
        try:
            runNextTest()
        except:
            print('FAILED',testName(),sys.exc_info()[:2],file=log)
        else:
            print('PASSED',testName(),file=log)
testdrive()

這裏的testdrive函數會循環進行一組測試調用(在這個例子中,模塊testapi是抽象的)。因爲測試案例中未被捕捉的異常,一般都會終止這個測試驅動程序,如果想在測試失敗後讓測試進程繼續下區,就需要在try中包裝測試案例的調用。就像往常一樣,空的except會捕捉由測試案例所產生的沒有被捕捉的異常,而其使用sys.exc_info把該異常記錄到文件內。沒有異常發生時(測試成功情況),else分句就會執行。

對於作爲測試驅動運行在同一個進程的函數、模塊以及類,而進行測試的系統而言,這種形式固定的代碼是很典型的。然而,在實際應用中,測試可能會比這裏演示的跟爲精緻複雜。例如,要測試外部程序時,要改爲檢查程序啓動工具所產生的狀態代碼或輸出,例如,標準庫手冊哩談到的os.system和os.popen(這類工具一般不會替外部程序中的錯誤引發異常。事實上,測試案例可能會和測試驅動並行運行)。

關於sys.exc_info

sys.exc_info通常允許一個異常處理器獲取對最近引發的異常的訪問。當使用空的except子句來盲目地捕獲每個異常以確定引發了什麼的時候,這種方式特別有用:

try:
    ...
except:
    # sys.exc_info()[0:2] are the exception class and instance

如果沒有處理器正在處理,就返回包含了三個None值的元組。否則,將會返回(type、value和traceback):

  • type是正在處理的異常的異常類型
  • value是引發的異常類實例
  • traceback是一個traceback對象,代表異常最初發生時所調用的堆棧。

當捕獲異常分類超類的時候,sys.exc_info對於確定特定的異常類型很有用。由於在這種情況下,也可以通過as子句所獲取的實例__class__屬性來獲得異常類型,sys.exc_info如今主要由空的except使用:

try:
    ...
except General as instance:
    # instance.__class__ is the exception class

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

try:
    ...
except General as instance:
    # instance.method() does the right thing for this instance

通常,在Python中太具體可能會限制代碼的靈活性。像這裏的最後一個例子的多態方案,通常根號地支持未來的改進。

與異常有關的技巧

應該包裝什麼

從理論上講,可以在腳本中把所有的語句都包裝在try中,但這樣做不夠明治。這其實是設計的問題,不在語言本身的範圍內,而實際運用時,就會更明顯。以下時一些簡要的原則。

  • 經常會失敗的運算一般都應該包裝在try語句內。例如,和系統狀態銜接的運算(文件開啓、套接字調用等)就是try的主要候選者。
  • 但上邊的規則也有些特例:在簡單的腳本中,會希望這類運算失敗時終止程序,而不是被捕捉或是被忽略。如果是一個重大的錯誤,更是如此。Python中的錯誤會產生有用的出錯消息(如果不是崩潰的話),而且這通常就是所期望的最好結果。
  • 應該在try/finally中實現終止動作,從而保證它們的執行,除非環境管理器作爲一個with/as選項可用。這個語句的形式可以執行代碼,無論異常是否發生。
  • 偶爾,把對大型函數的調用包裝在單個try語句內,而不是讓函數本身零散着放入若干try語句中,這樣會更方便。這樣的話,函數中的異常就會往上傳遞到調用周圍的try,而程序員也可以減少函數中的代碼量。

捕捉太多:避免空except的語句

另一個問題是處理器的通用性問題。Python可選擇喲啊捕捉哪些異常,有時候必須小心,不要涵蓋太廣。

空except很容易編寫,有時候也是想要的結果,但是可能攔截到異常嵌套結構中較高層的try處理器所期待的事件。例如,下列的異常處理器,會捕捉每個到達的異常並使其停止,無論是否有另一個處理器在等待該事件。

def func():
    try:
        ... # IndexError is raised in here
    except:
        ... # But everything comes here and dies!
try:
    func()
except IndexError: # Exception should be precessed here
    ...

也許更糟,這類代碼可能會捕捉無關的系統異常。而這類異常通常是不應該攔截的。

例如,當控制權到達頂層文件末尾時,腳本正常時退出的。然而,Python也提供內置sys.exit(statuscode)調用來提前終止。這實際上是引發內置的SystemExit異常來終止程序,使tyr/finally可以在離開前執行,而程序的特殊類型可攔截該事件。因此,try帶空except時,可能會不知不覺阻止重要的結束。如下面文件所示:

import sys

def bye():
    sys.exit(40) # Crucial error :abort now!
try:
    bye()
except:
    print('got it') # Oops--we ignored the exit
print('contining')
got it
contining

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

try:
    bye()
except Exception: # Won't catch exits,but will catch many others
    ...

在其他情況下,這種方案並不比空的except子句好——因爲Exception是所有內置異常(除了系統退出事件以外)之上的一個超類,它仍然有潛力捕獲程序中其他地方的異常。

最糟的情況是,空except和捕獲Exception類也會捕捉一般程序設計錯誤,但這類錯誤多數時候都應讓其通過。事實上,這兩種技術都會有效關閉Python的錯誤報告機制,使得代碼中的錯誤難以發現。例如,考慮下面的代碼:

mydictionary={...}
...
try:
    x=myditctionary['spam'] # Oops:misspelled
except:
    x=None  # Assume we got KeyError
...continue here with x...

在這裏代碼的編寫者假設,對字典做字典運算時,唯一可能發生的錯誤就是鍵錯誤。但是,因爲變量名myditctionary乒協錯誤了(應該是mydictionary),Python會爲未定義的變量名的引用引發NameError,但處理器會默默地捕捉並忽略了這個異常。事件處理器錯誤填寫了字典錯誤的默認值,導致了程序出錯。如果這件事是發生在離讀取值的使用很遠的地方,就會編程一項很有趣的調試任務!

經驗法則是,儘量讓處理器具體化:空except子句很方便,但是可能容易出錯。

捕捉過少:使用基於類的分類

另一方面,處理器也不應過於具體化。當在try中列出特定的異常時,就只捕捉實際所列出的事件。這不見得是壞事,如果系統演進發展,以後會引發其他的異常,就得回頭在代碼其他地方,把這些新的異常加入異常的列表。

例如,下面的處理器把MyExcept1和MyExcept2看作是正常的情況,而把其他一切視爲錯誤。但若將來增加了MyExcept3,就會被視爲錯誤並處理,除非更新異常列表:

try:
    ...
execpt (MyExcept1,MyExcept2): # Breaks if add a MyExcept3
    ... # Non-errors
else:
    ... # Assumed to be an error

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

try:
    ...
except SuccessCategoryNme: # Ok if add a myerror3 subclass
    ... # Non-errors
else:
    ... # Assumed to be an error

無論是否使用基於類的異常的分類層次,採用一點細微的設計,就可以走得長遠。這個故事的寓意是:異常處理器不要過於一般化,也不要太過於具體化,而且要明智選擇try語句所包裝的代碼量。特別是在較大系統中,異常規則也應該是整體設計的一部分。

核心語言總結

這是對Python這個語言整個核心部分的一個回顧!

當你看到這的話,將來在簡歷上就可以自信地加上Python了!

Python工具集

一般而言,Python提供了一個有層次的工具集。

  • 內置工具:像字符串、列表以及字典這些內置類型,會讓編寫簡單的程序更爲迅速。
  • Python擴展:更重要的任務來說,可以編寫自己的函數、模塊以及類來擴展Python
  • 已編譯的擴展:雖然這裏不會介紹這一話題,但Python也可以使用C或C++這樣的外部語言所編寫的模塊來進行擴展。

因爲Python講其工具集分層,可以決定程序任務要多麼的深入這個層次:可以讓簡單腳本使用內置工具,替較大系統新增Python所編寫的擴展工具,並且爲高級工作編寫那些編譯好的擴展工具。

下表總結了Python程序員可用的內置或現有的功能來源,而有些話題可能會用剩餘的Python生涯事件來探索。到目前爲止,這裏所舉例的例子都很小、獨立完備。它們是有意這樣編寫的,也就是爲了能夠幫助學習者掌握基礎知識。但既然瞭解核心語言知識的,就該是學習如何使用Python內置接口進行實際工作的時候了。你會發現,利用Python這種簡單的語言,常見任務會比想象的更爲簡單。

Python的工具箱類型

分類 例子
對象類型 列表、字典、文件和字符串
函數 len、range、open
異常 IndexError、KeyError
模塊 os、tkinter、pickle、re
屬性 __dict__、__name__、__class__
外部工具 NumPy、SWIG、Jython、IronPython、Django等

大型項目的開發工具

一旦精通了Python基礎知識,就會發現Python程序變得比你至今體驗過的例子還要大。對於開發大型系統而言,Python和公開領域有一批開發工具可以使用。你已經看過其中幾種工具的用法,而且本書也提到過一些。以下是此領域中一些最常用的工具的摘要。

PyDoc和文檔字符串

PyDoc的help函數和HTML界面在第15章介紹過。PyDoc爲模塊和對象提供了一個文檔系統,並整合了Python的文檔字符串功能。這是Python系統的標準部分,參考庫手冊以獲得更多細節。

PyChecker和Pylint

因爲Python是一門動態語言,所以有些程序設計的錯誤在程序運行前不會報錯(例如,當文件執行或導入時,語法錯誤會被捕捉)。這不是什麼大的缺點:就大多數語言一樣,這知識代表把產品傳送給客戶錢,需要測試你的Python程序。此外,Python的動態本質、自動出錯消息以及異常模型,使你很容易在Python中尋找和修改錯誤,遠勝過其他語言。

PyChecker和PyLint系統可以在腳本運行前把大量的常見錯誤預先緩存起來。這和C開發領域中的"lint"程序扮演了類似的角色。有些Python社區會在測試或者分發給客戶前,先用PyCHecker執行其代碼,來捕捉任何潛在的問題。事實上,Python標準庫在發佈前都會定期用PyChecker執行。PyChecker和Pylint是第三方開源代碼包。可以在http://www.python.org或者PyPI網站上找到它們。

PyUnit

Python有兩個測試輔助工具。第一個是PyUnit(在庫手冊中稱爲unittest),提供了一個面向對象的類框架,來定義和定製測試案例以及預期的結果。這是模擬JAVA的JUnit框架的。這是個精緻的類系統。

doctest

doctest標準庫模塊,提供第二個並且更爲簡單的做法來進行迴歸測試。這是基於Python的文檔字符串功能的。概括地講,要使用doctest時,把交互模式測試會話的記錄複製粘貼到源代碼文件中的文檔字符串中。然後,doctest回抽取出你的文檔字符串,分解出測試案例和結果,然後重新執行測試,並驗證預期的結果。doctest的操作可以用多種方式剪裁。

IDE

例如IDLE這類IDE,提供了圖形環境,來編輯、運行、調試以及瀏覽Python程序。有些高級的IDE支持其他的開發任務,包括源代碼控制整合、GUI交互構建工具和項目文件等。

配置工具

因爲Python是高級和動態的,所以從跟其他語言學習得到的之間經驗,通常不適用於Python代碼。爲了真正把代碼中的性能瓶頸隔離出來,需要通過time或timeit模塊內的時鐘工具新增計時邏輯,或者在profile模塊下運行代碼。

profile是標準庫模塊,爲Python實現源代碼配置工具。它會運行你所提供的代碼的字符串(例如,腳本文件的導入、或者對函數的調用)。在默認情況下,它會打印報告到標準輸出流,從而給出性能的統計結果:每個函數的調用次數、每個函數所化事件等。

profile模塊可以作爲一個腳本運行或導入,並且可以以多種方式進行定製。例如,可以把統計資料保存到文件中,稍後使用pstats模塊進行分析。要交互地進行配置,導入profile模塊並調用profile.run(‘code’),把想要配置的diamagnetic作爲一個字符串傳入(例如,對函數的一次調用或者對整個文件的導入)。要從一個系統shell命令行配置,使用一條形式爲python -m profile main.py args的命令。參閱Python標準庫手冊來了解其他的配置選項:例如,cProfile模塊,用於與profile相同的接口,但是運行起來負載較少,因此它可能適合於配置長時間運行的程序。

調試器

作爲一個回顧,Python的大多數開發IDE都支持基於GUI的調試,並且Python標準庫包含了一個命令行源代碼調試器模塊,稱爲pdb。這個模塊提供了與常用的C語言的調試器工作非常類似的一個命令行界面。

和配置器很相似,pdb調試器可以交互地運行,或者從一個命令行運行,並且可以從一個Python程序導入並調用。要交互地使用它,導入這個模塊,調用pdb函數開始執行代碼(例如,pdb.run(“main()”)),然後再交互模式提示符下輸入調試命令。要從一個系統shell命令行啓動pdb,使用形式爲python -m pdb main.py args…的一條命令。pdb包括了實用的事後分析調用,即pdb.pm(),它可在遇到異常後啓動調試器。

因爲IDLE這類IDE包括“指針並點擊”的調試界面,pdb其實現在很少有人使用。

發佈的選擇

py2exe、PyInstaller以及freeze都可以打包字節碼以及Python虛擬機,從而稱爲“凍結二進制”的獨立的可執行文件也就是不需要目標機器上有安裝Python,完全可以隱藏系統的代碼。此外,當Python程序分發個客戶時,可以採用源代碼的形式或字節碼的形式(.pyc),此外,導入鉤子支持特殊的打包技術,例如,zip文件自動解壓縮以及字節碼加密。distutils模塊,爲Python模塊和套件以及C編寫而成的擴展工具提供了各種打包選項,更多細節參考Python手冊。後起之秀Python eggs打包系統提供另一種做法,也可解決包的相互依賴性,更多細節請在互聯網上搜索。

優化選項

優化程序時,Psyco系統提供了實時的編譯器,可以把Python字節碼翻譯成二進制機碼,而Shedskin提供了Python對C++的翻譯器。偶爾會看見.pyo優化後的字節碼文件,這是以-o python命令標誌位運行所產生的文件,這隻提供了優先的性能提升,並不常用。最後,也可以把程序的一部分改寫爲用C這類變異性語言編寫,從而提高程序性能。一般來說,Python的速度也會隨着事件不斷改善,要儘可能升級到最新的版本。

對於大型項目的提示

在本書中遇到過各種語言特性,當開始編寫大型項目時,就會變得更有用。其中,模塊包(23章)、基於類的異常(33章)、類的僞私有屬性(30章)、文檔字符串(15章)、模塊路徑配置文件(21章從from *以及__all__列表到_X風格的變量名來隱藏變量名(24章)、以__name__==’__main__'技巧來增加自我測試代碼(24章)和使用函數和模塊的常見設計規則(17、19、24章),使用面向對象設計模式(30章及其他),等等。

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