2020年10月9日星期五

009 零基礎學的Python 第一篇 Python語言基礎 第9章 異常處理與程式調試

申明本站飛宇網 https://feiyetopro.blogspot.com/自網路收集整理之書籍文章影音僅供預覽交流學習研究,其[書籍、文章、影音]情節內容, 評論屬其個人行為, 與本網站無關。版權歸原作者和出版社所有,請在下載 24 小時內刪除,不得用作商業用途;如果您喜歡其作品,請支持訂閱購買[正版]謝謝!


9章 異常處理與程式調試

本章將介紹Python的異常處理。Python標準庫的每個模組都使用了異常,異常在Python中除了可以捕獲錯誤,還有一些其他用途。當程式中出現異常或錯誤時,最後的解決辦法就是偵錯工具。PythonIDE工具提供了不同程度的程式調試。PyCharm的程式調試功能比較強大,本章將結合具體的例子講解程式的調試,包括中斷點、單步等調試方法。

本章的知識點:

·tryexcept語句

·raise語句

·assert語句

·自訂異常

·程式調試

 9.1 異常的處理

異常是任何語言必不可少的一部分。Python提供了強大的異常處理機制,通過捕獲異常可以提高程式的健壯性。異常處理還具有釋放物件、中止迴圈的運行等作用。

9.1.1 Python中的異常

異常(Exception)是指程式中的例外、違例情況。異常機制是指當程式出現錯誤後,程式的處理方法。異常機制提供了程式正常退出的秘密頻道。當出現錯誤後,程式執行的流程發生改變,程式的控制權轉移到異常處理器,如序列的下標越界、打開不存在的文件、空引用異常等。當異常被引發時,如果沒有代碼處理該異常,異常將被Python接收處理。當異常發生時,Python解譯器將輸出一些相關的資訊並終止程式的運行。

Python3中,BaseException是所有異常類的基類,所有的內置異常都是它的派生類。Exception是除了SystemExitGeneratorExitKeyboardInterrupt之外的所有內置異常的基類,用戶自訂的異常也應該繼承它。它包括以下異常:

·StopItertion,當反覆運算器中沒有資料項目時觸發,由內置函數next()和反覆運算器的__next__()方法觸發。

·ArithmeticError,演算法異常的基類,包括OverflowError(溢出異常)、ZeroDivisionError(零除異常)和FloatingPointError(失敗的浮點數操作)。

·AssertionErrorassert語句失敗時觸發。

·AttributeError,屬性引用和屬性賦值異常。

·BufferError,緩存異常,當一個緩存相關的操作不能進行時觸發。

·EOFError,檔末尾,使用內置函數input()時生成,表示到達檔末尾。但是如read()readline()等大多數I/O操作將返回一個空字串來表示EOF,而不是引發異常。

·ImportError,導入異常,當import語句或者from語句無法在模組中找到相應檔案名稱時觸發。

·LookupError,當使用映射或者序列時,如果鍵值或者索引無法找到的時候觸發。它是KeyError(映射中未找到鍵值)和IndexError(序列下標超出範圍)的基類。

·MemoryError,記憶體錯誤,當操作超出記憶體範圍時觸發。

·NameError,名稱異常,在局部或者全域空間中無法找到檔案名稱時觸發。

·OSError,當一個系統函數返回一個系統相關的錯誤時觸發。在Python3.3中,Environment-ErrorIOErrorWindowsErrorVMSErrorsocket.errorselect.errormmap.error也整合進了OSError

·ReferenceError,引用異常,當底層的物件被銷毀後訪問弱引用時觸發。

·RuntimeError,包含其他分類中沒有被包括進去的一般錯誤。

·SyntaxError,語法錯誤,在import時也可能觸發。

·SystemError,編譯器的內部錯誤。

·TypeError,類型異常,當操作或者函數應用到不合適的類型時觸發。

·ValueError,值異常,當操作或者函數的類型正確,但是值不正確時觸發。

·Warning,警告,Python中有一個warnings模組,用來通知程式師不支持的功能。

Python內的異常使用繼承結構創建,這種設計方式非常靈活。可以在例外處理常式中捕獲基類異常,也可以捕獲各種子類異常。Python使用tryexcept語句捕獲異常,異常類型定義在try子句的後面。

注意  如果在except子句後將異常類型設置為Exception,例外處理常式將捕獲除程式中斷外的所有異常。因為Exception類是其他異常類的基類。Python3中移除了StandardError,使用Exception代替。

9.1.2 tryexcept的使用

tryexcept語句用於處理問題語句,捕獲可能出現的異常。try子句中的代碼塊放置可能出現異常的語句,except子句中的代碼塊處理異常。當異常出現時,Python會自動生成1個異常物件。該物件包括異常的具體資訊,以及異常的種類和錯誤位置。例如,當以下代碼讀取1個不存在的檔時,解譯器將提示異常。


open("hello.txt", "r")


異常資訊如下所示。


FileNotFoundError: [Errno 2] No such file or directory: 'hello.txt'


hello.txt不存在時,程式出現了例外,解譯器提示FileNotFoundError異常。為了使程式更友好,可以添加tryexcept語句捕獲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下面的代碼塊也不會執行。

tryexcept語句後還可以添加1else子句。當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子句將被執行。

PythonJava處理異常的模式相似,異常處理語句也可以嵌套。


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行代碼嵌套了1try子句。

·第5行代碼,字串s的元素執行減法運算。由於字串物件不支援減法運算,所以該語句將引發異常。

·第6行代碼,捕獲類型錯誤異常,輸出結果:“字串不支援減法運算”。

注意  如果外層try子句中的代碼塊引發異常,程式將直接跳轉到外層try對應的excpet子句,而內部的try子句的代碼塊將不會被執行。

tryexcept嵌套語句通常用於釋放已經創建的系統資源。

9.1.3 tryfinally的使用

tryexcept語句後還可以添加1finally子句,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子句都必須有1except子句或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__()初始化。

·第67行代碼定義兩個屬性,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的長度應為5assert引發的異常資訊如下所示。


Traceback (most recent call last):

  File "assert.py", line 8, in <module>

    assert len(t) == 1

AssertionError


assert語句還可以傳遞提示資訊給AssertionError異常。當assert語句斷言失敗時,提示資訊將列印到控制台。下面這段代碼檢測月份的輸入範圍。


01      # messageassert語句

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說明了程式的執行順序和異常的輸出順序。


9-1 程式的執行順序和異常的輸出順序

由於程式中沒有使用tryexcept捕獲異常,程式將中斷執行。可參考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偵錯工具

IDLEPython自帶的一個簡易的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()))



9-2 IDLE交互環境


9-3 Debug Control介面

輸入完成後,打開Debugger,並作相應的設置,主要是勾選想要查看的內容。這裡將4個選項全部勾選。在交互環境Python Shell中運行想要執行的代碼,此時會發現Debug Control中有了相應的變化,第一塊源碼區顯示的是當前執行到的源碼,區域變數Locals此時還沒有資料,顯示None,全域變數Globals顯示當前載入進的所有全域變數。此時便可以根據需要來進行程式調試了。按一下Go會執行完整個程式並輸出最後的結果。按一下Step可以一步步地執行代碼,並在Debug Control中查看各步驟的執行過程,包括當前執行的內部源碼、各個變數的當前值等。OverOut在調試迴圈時很有用,分別用於進入和跳出迴圈,此處不作介紹。當調試完成時可以在交互環境中看到最終的結果。

注意  如果不按一下Quit退出,該程式一直在執行中。

而通常我們需要調試的是Python指令檔,IDLE同樣支持,並且支持設置中斷點等。按一下【File|New File】或者按快速鍵Ctrl+N打開新檔,在新檔中編寫代碼,然後保存。此處保存為traceback.py,如圖9-4所示。


9-4 保存為tranceback.py文件

在檔中想要設置中斷點的地方右擊,然後選擇【Set Breakpoint】便可以再次設置中斷點,該行將會變為黃色背景。設置好中斷點之後,回到交互環境,首先使用import語句將該檔導入,該檔被視為一個Python模組,注意不需要帶上尾碼名。


import traceback


同樣的,打開Debugger,此時指令檔中的所有內容都被引入了。之後的操作與前面所述相同。

 9.3 使用Easy Eclipse for Python偵錯工具

PythonWin偵錯工具的操作與常用開發工具的使用習慣不同。在Dug命令列中才能查看程式中變數的值,使用起來不夠方便。Eclipse作為萬能開發工具支援多種語言,包括CJavaPHPPython等。可喜的是在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所示。


9-5 工作空間的路徑設置


9-6 新建Pydev Project1

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


9-7 新建Pydev Project2

最後按一下【Finish】按鈕,完成Debug工程的創建。在src目錄下新建Pydev Module,檔命名為trace.pytrace.py中的代碼為9.2節的偵錯工具。

9.3.2 配置調試

在偵錯工具之前,需要設置Python解譯器的路徑,並導入Python環境變數下包含的庫檔。打開【Window|Preferences】菜單,彈出如圖9-8所示的【Preferences】對話方塊。【Preferences】對話方塊可以對Eclipse的開發環境和各種外掛程式進行設置,其中節點Pydev就是Python外掛程式的設置項。

展開節點Pydev後選中Interpreter Python子節點。然後按一下【New】按鈕,加入python25.exepythonw25.exe所在的路徑。最後按一下【Apply】按鈕,Eclipse將自動載入Python環境變數下包含的庫檔。


9-8 【Preferences】對話方塊

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


9-9 【Debug】對話方塊

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


9-10 偵錯工具的配置(1

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


9-11 偵錯工具的配置(2

最後按一下【Debug】按鈕就可以把trace.py切換到調試模式下。程式中沒有設置任何中斷點,此時的調試並不能查看程式運行的狀態,下面將在程式中添加中斷點。

9.3.3 設置中斷點

在文件trace.py的第10行代碼處設置中斷點,然後按一下Eclipse工具列中的Debug按鈕啟動偵錯工具。trace.py的調試模式設置成功後,將在Debug按鈕的下拉式功能表中出現trace子功能表,如圖9-12所示。按一下trace子功能表也可以啟動偵錯工具。

當程式運行到中斷點處時,Eclipse將自動切換到Debug視窗下。【Breakpoints】標籤頁顯示了當前程式中的中斷點資訊,如圖9-13所示。


9-12 啟動偵錯工具


9-13 trace.pyDebug調試窗口

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


9-14 在Debug調試視窗中查看變數的值

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


9-15 異常資訊的輸出

按一下【Console】標籤頁最後一處的連結,Eclipse將直接定位到異常發生的源點。把變數b的值改為非零值即可修正異常。

 9.4 小結

本章介紹了Python中異常的處理,重點講解了Python3.3異常的組織結構以及tryexcepttryfinallyraiseassert語句的使用。Python中的traceback物件可以記錄異常資訊和當前程式的狀態。當異常發生時,traceback物件將輸出異常資訊。異常資訊應從下往上閱讀,可以快速地定位出錯的行號。最後講解了程式調試的一般步驟以及中斷點設置、單步、跳出等調試方法。結合ZeroDivisionError異常講解了自帶IDLEEclipseIDE工具的調試功能。當程式出現問題後,調試是解決問題的常用手段。熟練使用IDE工具進行調試可以提高程式開發的效率。

 9.5 習題

1.選擇自己喜歡的工具,練習調試方法。

2.把前面學習過的例子,都用本章學習的方法增加異常處理。

 


0 留言:

發佈留言