
本章將介紹Python的異常處理。Python標準庫的每個模組都使用了異常,異常在Python中除了可以捕獲錯誤,還有一些其他用途。當程式中出現異常或錯誤時,最後的解決辦法就是偵錯工具。Python的IDE工具提供了不同程度的程式調試。PyCharm的程式調試功能比較強大,本章將結合具體的例子講解程式的調試,包括中斷點、單步等調試方法。
本章的知識點:
·try…except語句
·raise語句
·assert語句
·自訂異常
·程式調試
異常是任何語言必不可少的一部分。Python提供了強大的異常處理機制,通過捕獲異常可以提高程式的健壯性。異常處理還具有釋放物件、中止迴圈的運行等作用。
9.1.1 Python中的異常
異常(Exception)是指程式中的例外、違例情況。異常機制是指當程式出現錯誤後,程式的處理方法。異常機制提供了程式正常退出的秘密頻道。當出現錯誤後,程式執行的流程發生改變,程式的控制權轉移到異常處理器,如序列的下標越界、打開不存在的文件、空引用異常等。當異常被引發時,如果沒有代碼處理該異常,異常將被Python接收處理。當異常發生時,Python解譯器將輸出一些相關的資訊並終止程式的運行。
在Python3中,BaseException是所有異常類的基類,所有的內置異常都是它的派生類。Exception是除了SystemExit、GeneratorExit和KeyboardInterrupt之外的所有內置異常的基類,用戶自訂的異常也應該繼承它。它包括以下異常:
·StopItertion,當反覆運算器中沒有資料項目時觸發,由內置函數next()和反覆運算器的__next__()方法觸發。
·ArithmeticError,演算法異常的基類,包括OverflowError(溢出異常)、ZeroDivisionError(零除異常)和FloatingPointError(失敗的浮點數操作)。
·AssertionError,assert語句失敗時觸發。
·AttributeError,屬性引用和屬性賦值異常。
·BufferError,緩存異常,當一個緩存相關的操作不能進行時觸發。
·EOFError,檔末尾,使用內置函數input()時生成,表示到達檔末尾。但是如read()和readline()等大多數I/O操作將返回一個空字串來表示EOF,而不是引發異常。
·ImportError,導入異常,當import語句或者from語句無法在模組中找到相應檔案名稱時觸發。
·LookupError,當使用映射或者序列時,如果鍵值或者索引無法找到的時候觸發。它是KeyError(映射中未找到鍵值)和IndexError(序列下標超出範圍)的基類。
·MemoryError,記憶體錯誤,當操作超出記憶體範圍時觸發。
·NameError,名稱異常,在局部或者全域空間中無法找到檔案名稱時觸發。
·OSError,當一個系統函數返回一個系統相關的錯誤時觸發。在Python3.3中,Environment-Error、IOError、WindowsError、VMSError、socket.error、select.error和mmap.error也整合進了OSError。
·ReferenceError,引用異常,當底層的物件被銷毀後訪問弱引用時觸發。
·RuntimeError,包含其他分類中沒有被包括進去的一般錯誤。
·SyntaxError,語法錯誤,在import時也可能觸發。
·SystemError,編譯器的內部錯誤。
·TypeError,類型異常,當操作或者函數應用到不合適的類型時觸發。
·ValueError,值異常,當操作或者函數的類型正確,但是值不正確時觸發。
·Warning,警告,Python中有一個warnings模組,用來通知程式師不支持的功能。
Python內的異常使用繼承結構創建,這種設計方式非常靈活。可以在例外處理常式中捕獲基類異常,也可以捕獲各種子類異常。Python使用try…except語句捕獲異常,異常類型定義在try子句的後面。
注意 如果在except子句後將異常類型設置為Exception,例外處理常式將捕獲除程式中斷外的所有異常。因為Exception類是其他異常類的基類。Python3中移除了StandardError,使用Exception代替。
9.1.2 try…except的使用
try…except語句用於處理問題語句,捕獲可能出現的異常。try子句中的代碼塊放置可能出現異常的語句,except子句中的代碼塊處理異常。當異常出現時,Python會自動生成1個異常物件。該物件包括異常的具體資訊,以及異常的種類和錯誤位置。例如,當以下代碼讀取1個不存在的檔時,解譯器將提示異常。
open("hello.txt",
"r")
異常資訊如下所示。
FileNotFoundError: [Errno 2] No such
file or directory: 'hello.txt'
當hello.txt不存在時,程式出現了例外,解譯器提示FileNotFoundError異常。為了使程式更友好,可以添加try…except語句捕獲FileNotFoundError異常。修改後的程式碼如下所示。
01
try:
02 open("hello.txt",
"r") # 嘗試讀取一個不存在的檔
03 print ("讀文件")
04
except FileNotFoundErrorr:
# 捕獲FileNotFoundError異常
05 print ("文件不存在")
06
except:
# 其他異常情況
07 print ("程式異常")
【代碼說明】
·第1行代碼使用try語句,open("hello.txt","r")是可能出現問題的語句。
·第2行代碼,由於檔hello.txt不存在,Python引發異常,程式直接跳轉到第4行代碼。
·第3行代碼,此行代碼將不會執行。
·第4行代碼,文件不存在,觸發了FileNotFoundError異常。
·第5行代碼輸出結果為“檔不存在”。
·第6行代碼,如果try語句中出現了其他異常,將跳轉到此處。由於引發了IOError異常,所以except下面的代碼塊也不會執行。
try…except語句後還可以添加1個else子句。當try子句中的代碼發生異常時,程式直接跳轉到except子句;反之,程式將執行else子句。例如,執行除法運算時,當除數為0,將拋出ZeroDivisionError異常。
01
try:
02 result = 10/0
03
except ZeroDivisionError:
# 捕獲除數為0的異常
04 print ("0不能被整除")
05
else:
# 若沒有觸發異常則執行以下代碼
06 print (result)
【代碼說明】
·第2行代碼執行運算式10/0。
·第3行代碼,捕獲除數為0的異常。
·第4行代碼,由於除數為0,輸出結果:“0不能被整除”。
·第5行代碼,else子句中的代碼塊將不會被執行。如果第2行代碼的運算式為10/2,則異常不會發生,else子句將被執行。
Python與Java處理異常的模式相似,異常處理語句也可以嵌套。
01
try:
02 s = "hello"
03 try: # 嵌套異常
04 print (s[0] + s[1])
05 print (s[0] - s[1])
06 except TypeError:
07 print ("字串不支援減法運算")
08
except:
09 print ("異常")
【代碼說明】
·第3行代碼嵌套了1個try子句。
·第5行代碼,字串s的元素執行減法運算。由於字串物件不支援減法運算,所以該語句將引發異常。
·第6行代碼,捕獲類型錯誤異常,輸出結果:“字串不支援減法運算”。
注意 如果外層try子句中的代碼塊引發異常,程式將直接跳轉到外層try對應的excpet子句,而內部的try子句的代碼塊將不會被執行。
try…except嵌套語句通常用於釋放已經創建的系統資源。
9.1.3 try…finally的使用
try…except語句後還可以添加1個finally子句,finally子句的作用與Java中的finally子句類似。無論異常是否發生,finally子句都會被執行。所有finally子句通常用於關閉因異常而不能釋放的系統資源。9.1.2一節中的第1段代碼捕獲了打開不存在檔的異常,但是並沒有顯示的關閉打開的檔資源。在這段代碼中添加finally子句,修改後的代碼如下所示。
01
# finally錯誤的用法
02
try:
03 f = open("hello.txt",
"r")
04 print ("讀文件")
05
except FileNotFoundError: # 捕獲FileNotFoundError異常
06 print ("文件不存在")
07
finally: # 其他異常情況
08 f.close()
【代碼說明】
·第3行代碼,在try子句中打開文件hello.txt,並返回引用f。變數f只在try語句內有效,屬於區域變數。
·第5行代碼捕獲到IOError異常,輸出結果:“文件不存在”。
·第8行代碼,在finally子句中關閉打開的資源。該語句中的變數f並不是try語句中的f,因此解譯器認為變數f沒有定義。Python提示如下異常:
NameError: name 'f' is not defined
因此需要把檔的打開操作置於try語句的外層,使變數f具有全域性,同時也要捕獲檔打開的異常。這種情況就可以使用異常嵌套的語句,每個try子句都必須有1個except子句或finally子句與之對應。修改後的代碼如下所示。
01
try:
02 f = open("hello.txt",
"r")
03 try:
04 print (f.read(5))
05 except:
06 print ("讀取檔錯誤")
07 finally: # finally子句一般用於釋放資源
08 print ("釋放資源")
09
f.close()
10
except FileNotFoundError:
11 print ("文件不存在")
【代碼說明】
·第2行代碼在外層的try子句中打開文件hello.txt。
·第4行代碼,讀取檔hello.txt,並放置在內層try子句中。
·第5行代碼,如果讀取檔出現例外,將輸出“讀取檔錯誤”。
·第7行代碼,釋放資源。由於變數f定義在外層的try子句中,因此內層的finally子句可以使用變數f。無論檔讀取的異常是否發生,f.close()語句都將被執行。
注意 由於Python動態語言的特殊性,如果要在某個代碼塊中使用同一級其他代碼塊中定義的變數,可以考慮嵌套的方式或全域變數來實現。
9.1.4 使用raise拋出異常
當程式中出現錯誤時,Python會自動引發異常,也可以通過raise語句顯示的引發異常。一旦執行了raise語句,raise語句後的代碼將不能被執行。下面這段代碼演示了raise語句的使用方法。
01
try:
02 s = None
03 if s is None:
04 print ("s是空對象")
05 raise NameError
06 print (len(s))
07
except TypeError:
08 print ("空物件沒有長度")
【代碼說明】
·第2行代碼創建了變數s,該變數的值為空。
·第3行代碼判斷變數s的值是否為空。如果為空,則拋出異常NameError。
·第6行代碼,由於引發了NameError異常,所以該行代碼以及後面的代碼將不會被執行。
如果去掉第5行代碼,程式執行到第6行代碼時將引發TypeError異常。None物件不能使用函數len()調用。Raise語句通常用於拋出自訂異常。因為自訂異常並不在Python的控制範圍內,不會被Python自動拋出,應使用raise語句手工拋出。
9.1.5 自訂異常
Python允許程式師自訂異常類型,用於描述Python異常體系中沒有涉及的異常情況。自訂異常必須繼承Exception類。自訂異常按照命名規範以Error結尾,顯式地告訴程式師該類是異常類。自訂異常使用raise語句引發,而且只能通過手工方式觸發。下面這段代碼演示了自訂異常的使用。
01
from __future__ import division
02
03
class DivisionException(Exception):
# 自訂異常
04 def __init__(self, x, y):
05 Exception.__init__(self, x, y)
06 self.x = x
07 self.y = y
08
09
if __name__ ==
"__main__":
10 try:
11 x = 3
12 y = 2
13 if x % y > 0:
14 print (x / y)
15
raise
DivisionException(x, y) # 拋出異常
16 except DivisionException, div:
17 print ("DivisionException: x
/ y = %.2f" % (div.x / div.y))
【代碼說明】
·第3行代碼定義了自訂異常DivisionException,該異常繼承自Exception。
·第4行代碼在構造函數__init__()中調用基類的__init__()初始化。
·第6、7行代碼定義兩個屬性,x表示被除數,y表示除數。
·第13行代碼判斷x除以y的餘數是否大於0。如果大於0,則表示x不能整除y,將手工拋出自訂異常DivisionException。
·第15行代碼,由於引發了DivisionException異常,所以except子句也將被執行。
·第16行代碼,div表示DivisionException類的實例物件。
·第17行代碼調用div的屬性x除以屬性y。輸出結果:
DivisionException: x / y = 1.50
如果注釋了第15行代碼,DivisionException異常將不會被觸發。
9.1.6 assert語句的使用
assert語句用於檢測某個條件運算式是否為真。assert語句又稱為斷言語句,即assert認為檢測的運算式永遠為真。if語句中的條件判斷都可以使用assert語句檢測。例如,檢測某個元組中元素的個數是否大於1。如果assert語句斷言失敗,會引發AssertionError異常。
01
t = ("hello",)
02
assert len(t) >= 1
03
t = ("hello")
04
assert len(t) == 1
【代碼說明】
·第1行代碼定義了包含元素“hello”的元組t。
·第2行代碼檢測元組t的元素個數是否大於或等於1。該行代碼斷言成功。
·第3行代碼定義了1個序列t。由於圓括號中唯一的元素後沒有逗號,Python把該序列作為字串處理。
·第4行代碼檢測序列t的元素個數是否等於1。該行代碼斷言失敗,序列t的長度應為5。assert引發的異常資訊如下所示。
Traceback (most recent call last):
File "assert.py", line 8, in <module>
assert len(t) == 1
AssertionError
assert語句還可以傳遞提示資訊給AssertionError異常。當assert語句斷言失敗時,提示資訊將列印到控制台。下面這段代碼檢測月份的輸入範圍。
01
# 帶message的assert語句
02
month = 13
03
assert 1 <= month <= 12, "month errors"
【代碼說明】
·第2行代碼定義變數month,並賦值為13。
·第3行代碼,當assert語句斷言失敗,則傳遞字串“month
errors”給AssertionError異常。assert引發的異常資訊如下所示。
Traceback (most recent call last):
File "assert.py", line 12, in <module>
assert 1 <= month <= 12, "month errors"
AssertionError: month errors
注意 Python支持形如“m<=x<=n”的運算式,等價於運算式“x>=m
and x<=n”。新的表達方式更符合習慣用法。
9.1.7 異常資訊
當程式出現錯誤時,Python都會輸出相關的異常資訊,並指出錯誤的行號和錯誤的程式碼。例如,執行除數為0的除法運算,將拋出ZeroDivisionError異常。
01
def fun():
# 除法運算
02 a = 10
03 b = 0
04 return a / b
05
06
def format():
# 格式化輸出
07 print ("a / b = " +
str(fun()))
08
09
if __name__ == "__main__":
10
format()
上述代碼先在主程序中調用format(),然後在format()中調用fun()。這段代碼將輸出如下所示的異常資訊。
Traceback (most recent call last):
File "traceback.py", line 13, in <module>
format()
File "traceback.py", line 10, in format
print "a / b = " + str(fun())
File "traceback.py", line 7, in fun
return a / b
ZeroDivisionError: integer division or
modulo by zero
程式執行時,Python將產生traceback物件,記錄異常資訊和當前程式的狀態。traceback物件先記錄主程序的狀態,然後記錄format()中的狀態,最後記錄fun()中的狀態。當fun()出現異常時,traceback物件將輸出記錄的資訊。因此,異常資訊應從下往上閱讀,最後1次出現的行號通常就是錯誤的發生處。從上述輸出資訊可以發現第7行代碼出現了異常。圖9-1說明了程式的執行順序和異常的輸出順序。

由於程式中沒有使用try…except捕獲異常,程式將中斷執行。可參考9.1.2節捕獲ZeroDivisionError異常。異常捕獲後,程式不會輸出任何資訊。為了便於程式師偵錯工具,需要輸出相關的異常資訊。在Python中可以直接輸出異常實例的內容。
01
import sys
02
03
try:
04 x = 10 / 0
05
except Exception as ex:
06 print (ex)
07 print (sys.exc_info())
【代碼說明】
·第4行代碼將出現異常,程式跳轉到第6行代碼。
·第6行代碼直接輸出Exception類的實例ex。輸出結果為division
by zero。
·第7行代碼調用sys模組的exc_info()輸出異常的類型、traceback物件等資訊。輸出結果:
division by zero
(<class 'ZeroDivisionError'>, ZeroDivisionError('division by zero',), <traceback object at 0x102ba2e60>)
9.2 使用自帶IDLE偵錯工具
IDLE是Python自帶的一個簡易的IDE,具有程式調試的功能,並且提供交互環境和指令檔調試兩種模式。打開IDLE後默認是一個交互環境,可以直接編寫Python代碼並且能即時查看輸出,如圖9-2所示。按一下【Debug】|【Debugger】會彈出Debug Control調試介面,如圖9-3所示。介面上有5個按鈕,分別為Go(運行)、Step(單步調試)、Over(跳過)、Out(跳出)和Quit(退出)。4個可選項,用於顯示調試時的Stack(堆疊)、Source(源碼)、Locals(區域變數)和Globals(全域變數)。
在按一下Debug打開時,交互介面會顯示DEBUG
ON,表示此時在調試狀態下,當關閉Debugger時,交互介面會顯示DEBUG
OFF表示此時已經關閉調試狀態。
下面以9.1.7節的除法程式為例,分別從交互環境和腳本兩種情況介紹IDLE的調試功能。
首先打開IDLE,然後在交互環境中輸入以下代碼:
01 def fun():
02
a = 10
03
b = 0
04
return a / b
05
def format():
06
print (“a / b = “ + str(fun()))
輸入完成後,打開Debugger,並作相應的設置,主要是勾選想要查看的內容。這裡將4個選項全部勾選。在交互環境Python Shell中運行想要執行的代碼,此時會發現Debug Control中有了相應的變化,第一塊源碼區顯示的是當前執行到的源碼,區域變數Locals此時還沒有資料,顯示None,全域變數Globals顯示當前載入進的所有全域變數。此時便可以根據需要來進行程式調試了。按一下Go會執行完整個程式並輸出最後的結果。按一下Step可以一步步地執行代碼,並在Debug Control中查看各步驟的執行過程,包括當前執行的內部源碼、各個變數的當前值等。Over和Out在調試迴圈時很有用,分別用於進入和跳出迴圈,此處不作介紹。當調試完成時可以在交互環境中看到最終的結果。
注意 如果不按一下Quit退出,該程式一直在執行中。
而通常我們需要調試的是Python指令檔,IDLE同樣支持,並且支持設置中斷點等。按一下【File】|【New
File】或者按快速鍵Ctrl+N打開新檔,在新檔中編寫代碼,然後保存。此處保存為traceback.py,如圖9-4所示。

在檔中想要設置中斷點的地方右擊,然後選擇【Set
Breakpoint】便可以再次設置中斷點,該行將會變為黃色背景。設置好中斷點之後,回到交互環境,首先使用import語句將該檔導入,該檔被視為一個Python模組,注意不需要帶上尾碼名。
import traceback
同樣的,打開Debugger,此時指令檔中的所有內容都被引入了。之後的操作與前面所述相同。
PythonWin偵錯工具的操作與常用開發工具的使用習慣不同。在Dug命令列中才能查看程式中變數的值,使用起來不夠方便。Eclipse作為“萬能”開發工具支援多種語言,包括C、Java、PHP、Python等。可喜的是在Eclipse開發環境中可以調試Python程式。下麵將以前面講解的traceback.py程式為例,說明Eclipse中程式的調試功能。
9.3.1 新建工程
Eclipse安裝後第一次運行將提示工作空間的路徑設置,工作空間就是存放Eclipse新建工程的目錄,如圖9-5所示。筆者在Easy Eclipse for Python的根目錄下創建了一個目錄Workspace,該目錄即為Eclipse的工程目錄。
在偵錯工具之前需要新建一個工程。按一下【File】|【New】|【Project】功能表選項,彈出【New
Project】對話方塊,選中Pydev項目中的Pydev Project,如圖9-6所示。


然後按一下【Next】按鈕,顯示如圖9-7所示的對話方塊。在該對話方塊中可以設置工程屬性,包括工程名、存儲位置、使用的Python標準庫等資訊。在【Project name】文字方塊中輸入工程名稱Debug。Project contents選項用於設置工程的存儲路徑。勾選Use default核取方塊後,按一下【Browse】按鈕可以更改工程的存儲位置。在Project
type選項中選擇python2.5,讀者可根據機器中安裝的Python庫的版本選擇。選中最後的核取方塊表示Debug工程創建後會生成一個src目錄,該目錄即為Python的原始程式碼目錄。

最後按一下【Finish】按鈕,完成Debug工程的創建。在src目錄下新建Pydev
Module,檔命名為trace.py。trace.py中的代碼為9.2節的偵錯工具。
9.3.2 配置調試
在偵錯工具之前,需要設置Python解譯器的路徑,並導入Python環境變數下包含的庫檔。打開【Window】|【Preferences】菜單,彈出如圖9-8所示的【Preferences】對話方塊。【Preferences】對話方塊可以對Eclipse的開發環境和各種外掛程式進行設置,其中節點Pydev就是Python外掛程式的設置項。
展開節點Pydev後選中Interpreter
Python子節點。然後按一下【New…】按鈕,加入python25.exe、pythonw25.exe所在的路徑。最後按一下【Apply】按鈕,Eclipse將自動載入Python環境變數下包含的庫檔。

下麵設置文件trace.py的調試參數,按一下【Run】|【Debug…】命令,彈出如圖9-9所示的【Debug】對話方塊。

選中其中的Python
Run選項,該選項用於調試Python程式。按一下左上角的“新建”圖示,將會在Python
Run下生成一個新的配置項,如圖9-10所示。Name表示調試項的名稱,讀者可以根據需要自訂,這裡填寫為trace。【Main】標籤頁下的Project表示需要調試的工程,這裡填寫前面創建的工程trace。Main Medule選項表示需要調試的Python檔。按一下【Apply】按鈕後,將自動帶出環境變數PYTHONPATH中的路徑。

切換到【Arguments】標籤頁,在Interpreter選項中選擇python25.exe的路徑作為解譯器,pythonw25.exe用於解釋GUI程式,如圖9-11所示。

最後按一下【Debug】按鈕就可以把trace.py切換到調試模式下。程式中沒有設置任何中斷點,此時的調試並不能查看程式運行的狀態,下面將在程式中添加中斷點。
9.3.3 設置中斷點
在文件trace.py的第10行代碼處設置中斷點,然後按一下Eclipse工具列中的Debug按鈕啟動偵錯工具。trace.py的調試模式設置成功後,將在Debug按鈕的下拉式功能表中出現trace子功能表,如圖9-12所示。按一下trace子功能表也可以啟動偵錯工具。
當程式運行到中斷點處時,Eclipse將自動切換到Debug視窗下。【Breakpoints】標籤頁顯示了當前程式中的中斷點資訊,如圖9-13所示。


其中【Debug】標籤頁顯示trace.py的主執行緒。【Outline】標籤頁顯示當前程式中定義的函數,trace.py中定義了fun()和format()這兩個函數。【Console】標籤頁用於顯示控制台的輸出,例如print語句的輸出和異常的顯示。按F5鍵進行單步調試,函數format()將調用函數fun()。切換到【Variables】標籤頁可以查看程式中變數的值。當程式單步運行到第7行時,可以查看到變數a、b此刻的值,如圖9-14所示。

按F8鍵程式將從函數fun()返回函數format()的print語句,此時【Console】標籤頁並沒有任何異常輸出。繼續按F5鍵進行單步調試,【Console】標籤頁將輸出如圖9-15所示的異常。

按一下【Console】標籤頁最後一處的連結,Eclipse將直接定位到異常發生的源點。把變數b的值改為非零值即可修正異常。
9.4 小結
本章介紹了Python中異常的處理,重點講解了Python3.3異常的組織結構以及try…except、try…finally、raise、assert語句的使用。Python中的traceback物件可以記錄異常資訊和當前程式的狀態。當異常發生時,traceback物件將輸出異常資訊。異常資訊應從下往上閱讀,可以快速地定位出錯的行號。最後講解了程式調試的一般步驟以及中斷點設置、單步、跳出等調試方法。結合ZeroDivisionError異常講解了自帶IDLE、Eclipse等IDE工具的調試功能。當程式出現問題後,調試是解決問題的常用手段。熟練使用IDE工具進行調試可以提高程式開發的效率。
9.5 習題
1.選擇自己喜歡的工具,練習調試方法。
2.把前面學習過的例子,都用本章學習的方法增加異常處理。


0 留言:
發佈留言