錯誤和異常

8. 錯誤和異常

至今爲止還沒有進一步的談論過錯誤信息,不過在你已經試驗過的那些例子中,可能已經遇到過一些。Python 中(至少)有兩種錯誤:語法錯誤和異常( syntax errors 和 exceptions )。

8.1. 語法錯誤

語法錯誤,也稱作解釋錯誤,可能是學習 Python 的過程中最容易犯的

>>> while True print 'Hello world'
  File "<stdin>", line 1, in ?
    while True print 'Hello world'
                   ^
SyntaxError: invalid syntax

解析器會重複出錯的行,並在行中最早發現的錯誤位置上顯示一個小“箭頭”。錯誤(至少是被檢測到的)就發生在箭頭 指向 的位置。示例中的錯誤表現在關鍵字 print 上,因爲在它之前少了一個冒號( ':' )。同時也會顯示文件名和行號,這樣你就可以知道錯誤來自哪個腳本,什麼位置。

8.2. 異常

即使是在語法上完全正確的語句,嘗試執行它的時候,也有可能會發生錯誤。在程序運行中檢測出的錯誤稱之爲 異常,它通常不會導致致命的問題,你很快就會學到如何在 Python 程序中控制它們。大多數異常不會由程序處理,而是顯示一個錯誤信息

>>> 10 * (1/0)
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
ZeroDivisionError: integer division or modulo by zero
>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: cannot concatenate 'str' and 'int' objects

錯誤信息的最後一行指出發生了什麼錯誤。異常也有不同的類型,異常類型做爲錯誤信息的一部分顯示出來:示例中的異常分別爲 零除錯誤(ZeroDivisionError ) ,命名錯誤( NameError) 和 類型 錯誤( TypeError )。打印錯誤信息時,異常的類型作爲異常的內置名顯示。對於所有的內置異常都是如此,不過用戶自定義異常就不一定了(儘管這是一個很有用的約定)。標準異常名是內置的標識(沒有保留關鍵字)。

這一行後一部分是關於該異常類型的詳細說明,這意味着它的內容依賴於異常類型。

錯誤信息的前半部分以堆棧的形式列出異常發生的位置。通常在堆棧中列出了源代碼行,然而,來自標準輸入的源碼不會顯示出來。

bltin-exceptions 列出了內置異常和它們的含義。

8.3. 控制異常

可以編寫程序來控制已知的異常。參見下例,此示例要求用戶輸入信息,一直到得到一個有效的整數爲止,而且允許用戶中斷程序(使用 Control-C 或其它什麼操作系統支持的操作);需要注意的是用戶生成的中斷會拋出 KeyboardInterrupt 異常。

>>> while True:
...     try:
...         x = int(raw_input("Please enter a number: "))
...         break
...     except ValueError:
...         print "Oops!  That was no valid number.  Try again..."
...

try 語句按如下方式工作。

  • 首先,執行 try 子句 (在 try 和 except 關鍵字之間的部分)。
  • 如果沒有異常發生, except 子句 在 try 語句執行完畢後就被忽略了。
  • 如果在 try 子句執行過程中發生了異常,那麼該子句其餘的部分就會被忽略。如果異常匹配於 except 關鍵字後面指定的異常類型,就執行對應的except子句。然後繼續執行 try 語句之後的代碼。
  • 如果發生了一個異常,在 except 子句中沒有與之匹配的分支,它就會傳遞到上一級 try 語句中。如果最終仍找不到對應的處理語句,它就成爲一個 未處理異常 ,終止程序運行,顯示提示信息。

一個 try 語句可能包含多個except子句,分別指定處理不同的異常。至多隻會有一個分支被執行。異常處理程序只會處理對應的try子句中發生的異常,在同一個 try 語句中,其他子句中發生的異常則不作處理。一個except子句可以在括號中列出多個異常的名字,例如

... except (RuntimeError, TypeError, NameError):
...     pass

最後一個 except 子句可以省略異常名,把它當做一個通配項使用。一定要慎用這種方法,因爲它很可能會屏蔽掉真正的程序錯誤,使人無法發現!它也可以用於打印一行錯誤信息,然後重新拋出異常(可以使調用者更好的處理異常):

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except IOError as (errno, strerror):
    print "I/O error({0}): {1}".format(errno, strerror)
except ValueError:
    print "Could not convert data to an integer."
except:
    print "Unexpected error:", sys.exc_info()[0]
    raise

try ... except 語句可以帶有一個 else子句 ,該子句只能出現在所有except子句之後。當try語句沒有拋出異常時,需要執行一些代碼,可以使用這個子句。例如

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except IOError:
        print 'cannot open', arg
    else:
        print arg, 'has', len(f.readlines()), 'lines'
        f.close()

使用 else 子句比在 try 子句中附加代碼要好,因爲這樣可以避免 try ... except 意外的截獲本來不屬於它們保護的那些代碼拋出的異常。

發生異常時,可能會有一個附屬值,作爲異常的 參數 存在。這個參數是否存在、是什麼類型,依賴於異常的類型。

在異常名(列表)之後,也可以爲 except 子句指定一個變量。這個變量綁定於一個異常實例,它存儲在 instance.args 的參數中。爲了方便起見,異常實例定義了 __str__() ,這樣就可以直接訪問過打印參數而不必引用 .args 。 這種做法不受鼓勵。相反,更好的做法是給異常傳遞一個參數(如果要傳遞多個參數,可以傳遞一個元組),把它綁定到 message 屬性。

一旦異常發生,它會在拋出前綁定所有指定的屬性。:

>>> try:
...    raise Exception('spam', 'eggs')
... except Exception as inst:
...    print type(inst)     # the exception instance
...    print inst.args      # arguments stored in .args
...    print inst           # __str__ allows args to printed directly
...    x, y = inst          # __getitem__ allows args to be unpacked directly
...    print 'x =', x
...    print 'y =', y
...
<type 'exceptions.Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs

對於未處理的異常,如果它有一個參數,那做就會作爲錯誤信息的最後一部分(“明細”)打印出來。

異常處理句柄不止可以處理直接發生在 try 子句中的異常,即使是其中(甚至是間接)調用的函數,發生了異常,也一樣可以處理。例如

>>> def this_fails():
...     x = 1/0
...
>>> try:
...     this_fails()
... except ZeroDivisionError as detail:
...     print 'Handling run-time error:', detail
...
Handling run-time error: integer division or modulo by zero

8.4. 拋出異常

程序員可以用 raise 語句強制指定的異常發生。例如

>>> raise NameError('HiThere')
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
NameError: HiThere

要拋出的異常由 raise 的唯一參數標識。它必需是一個異常實例或異常類(繼承自 Exception 的類)。

如果你需要明確一個異常是否拋出,但不想處理它, raise 語句可以讓你很簡單的重新拋出該異常。:

>>> try:
...     raise NameError('HiThere')
... except NameError:
...     print 'An exception flew by!'
...     raise
...
An exception flew by!
Traceback (most recent call last):
  File "<stdin>", line 2, in ?
NameError: HiThere

8.5. 用戶自定義異常

在程序中可以通過創建新的異常類型來命名自己的異常(Python 類的內容請參見  )。異常類通常應該直接或間接的從 Exception 類派生,例如

>>> class MyError(Exception):
...     def __init__(self, value):
...         self.value = value
...     def __str__(self):
...         return repr(self.value)
...
>>> try:
...     raise MyError(2*2)
... except MyError as e:
...     print 'My exception occurred, value:', e.value
...
My exception occurred, value: 4
>>> raise MyError('oops!')
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
__main__.MyError: 'oops!'

在這個例子中,Exception 默認的 __init__() 被覆蓋。新的方式簡單的創建 value 屬性。這就替換了原來創建 args 屬性的方式。

異常類中可以定義任何其它類中可以定義的東西,但是通常爲了保持簡單,只在其中加入幾個屬性信息,以供異常處理句柄提取。如果一個新創建的模塊中需要拋出幾種不同的錯誤時,一個通常的作法是爲該模塊定義一個異常基類,然後針對不同的錯誤類型派生出對應的異常子類。:

class Error(Exception):
    """Base class for exceptions in this module."""
    pass

class InputError(Error):
    """Exception raised for errors in the input.

    Attributes:
        expr -- input expression in which the error occurred
        msg  -- explanation of the error
    """

    def __init__(self, expr, msg):
        self.expr = expr
        self.msg = msg

class TransitionError(Error):
    """Raised when an operation attempts a state transition that's not
    allowed.

    Attributes:
        prev -- state at beginning of transition
        next -- attempted new state
        msg  -- explanation of why the specific transition is not allowed
    """

    def __init__(self, prev, next, msg):
        self.prev = prev
        self.next = next
        self.msg = msg

與標準異常相似,大多數異常的命名都以“Error”結尾。

很多標準模塊中都定義了自己的異常,用以報告在他們所定義的函數中可能發生的錯誤。關於類的進一步信息請參見  一章。

8.6. 定義清理行爲

try 語句還有另一個可選的子句,目的在於定義在任何情況下都一定要執行的功能。例如

>>> try:
...     raise KeyboardInterrupt
... finally:
...     print 'Goodbye, world!'
...
Goodbye, world!
KeyboardInterrupt

不管有沒有發生異常, finally子句 在程序離開 try 後都一定會被執行。當 try 語句中發生了未被 except 捕獲的異常(或者它發生在 except 或 else子句中),在 finally 子句執行完後它會被重新拋出。 try 語句經由 break ,continue 或 return 語句退 出也一樣會執行 finally 子句。以下是一個更復雜些的例子(在同 一個 try 語句中的 except 和 finally 子句的工作方式與 Python 2.5 一樣)

>>> def divide(x, y):
...     try:
...         result = x / y
...     except ZeroDivisionError:
...         print "division by zero!"
...     else:
...         print "result is", result
...     finally:
...         print "executing finally clause"
...
>>> divide(2, 1)
result is 2
executing finally clause
>>> divide(2, 0)
division by zero!
executing finally clause
>>> divide("2", "1")
executing finally clause
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<stdin>", line 3, in divide
TypeError: unsupported operand type(s) for /: 'str' and 'str'

如你所見, finally 子句在任何情況下都會執 行。 TypeError 在兩個字符串相除的時候拋出,未被 except 子句捕獲,因此在 finally 子句執行完畢後重新拋出。

在真實場景的應用程序中, finally 子句用於釋放外部資源(文件 或網絡連接之類的),無論它們的使用過程中是否出錯。

8.7. 預定義清理行爲

有些對象定義了標準的清理行爲,無論對象操作是否成功,不再需要該對象的時 候就會起作用。以下示例嘗試打開文件並把內容打印到屏幕上。:

for line in open("myfile.txt"):
    print line

這段代碼的問題在於在代碼執行完後沒有立即關閉打開的文件。這在簡單的腳本 裏沒什麼,但是大型應用程序就會出問題。 with 語句使得文件之類的對象可以 確保總能及時準確地進行清理。:

with open("myfile.txt") as f:
    for line in f:
        print line

語句執行後,文件 f 總會被關閉,即使是在處理文件中的數據時出錯也一樣。 其它對象是否提供了預定義的清理行爲要查看它們的文檔。

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