8. 錯誤和異常?

到目前為止,我們還沒有提到錯誤消息,但是如果你已經嘗試過那些例子,你可能已經看過了一些錯誤消息。 目前(至少)有兩種可區分的錯誤:語法錯誤異常

8.1. 語法錯誤?

語法錯誤又稱解析錯誤,可能是你在學習Python 時最容易遇到的錯誤:

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

解析器會輸出出現語法錯誤的那一行,并顯示一個“箭頭”,指向這行里面檢測到的第一個錯誤。 錯誤是由箭頭指示的位置 上面 的 token 引起的(或者至少是在這里被檢測出的):在示例中,在 print() 這個函數中檢測到了錯誤,因為在它前面少了個冒號 (':') 。文件名和行號也會被輸出,以便輸入來自腳本文件時你能知道去哪檢查。

8.2. 異常?

即使語句或表達式在語法上是正確的,但在嘗試執行時,它仍可能會引發錯誤。 在執行時檢測到的錯誤被稱為 異常,異常不一定會導致嚴重后果:你將很快學會如何在 Python 程序中處理它們。 但是,大多數異常并不會被程序處理,此時會顯示如下所示的錯誤信息:

>>> 10 * (1/0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't convert 'int' object to str implicitly

錯誤信息的最后一行告訴我們程序遇到了什么類型的錯誤。異常有不同的類型,而其類型名稱將會作為錯誤信息的一部分中打印出來:上述示例中的異常類型依次是:ZeroDivisionErrorNameErrorTypeError。作為異常類型打印的字符串是發生的內置異常的名稱。對于所有內置異常都是如此,但對于用戶定義的異常則不一定如此(雖然這是一個有用的規范)。標準的異常類型是內置的標識符(而不是保留關鍵字)。

這一行的剩下的部分根據異常類型及其原因提供詳細信息。

錯誤信息的前一部分以堆棧回溯的形式顯示發生異常時的上下文。通常它包含列出源代碼行的堆棧回溯;但是它不會顯示從標準輸入中讀取的行。

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

8.3. 處理異常?

可以編寫處理所選異常的程序。請看下面的例子,它會要求用戶一直輸入,直到輸入的是一個有效的整數,但允許用戶中斷程序(使用 Control-C 或操作系統支持的其他操作);請注意用戶引起的中斷可以通過引發 KeyboardInterrupt 異常來指示。:

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

try 語句的工作原理如下:

  • 首先,執行 try 子句tryexcept 關鍵字之間的(多行)語句)。

  • 如果沒有異常發生,則跳過 except 子句 并完成 try 語句的執行。

  • 如果在執行 try 子句時發生了異常,則跳過該子句中剩下的部分。 然后,如果異常的類型和 except 關鍵字后面的異常匹配,則執行 except 子句,然后繼續執行 try 語句之后的代碼。

  • 如果發生的異常和 except 子句中指定的異常不匹配,則將其傳遞到外部的 try 語句中;如果沒有找到處理程序,則它是一個 未處理異常,執行將停止并顯示如上所示的消息。

一個 try 語句可能有多個 except 子句,以指定不同異常的處理程序。 最多會執行一個處理程序。 處理程序只處理相應的 try 子句中發生的異常,而不處理同一 try 語句內其他處理程序中的異常。 一個 except 子句可以將多個異常命名為帶括號的元組,例如:

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

如果發生的異常和 except 子句中的類是同一個類或者是它的基類,則異常和 except 子句中的類是兼容的(但反過來則不成立 --- 列出派生類的 except 子句與基類不兼容)。 例如,下面的代碼將依次打印 B, C, D

class B(Exception):
    pass

class C(B):
    pass

class D(C):
    pass

for cls in [B, C, D]:
    try:
        raise cls()
    except D:
        print("D")
    except C:
        print("C")
    except B:
        print("B")

請注意如果 except 子句被顛倒(把 except B 放到第一個),它將打印 B,B,B --- 即第一個匹配的 except 子句被觸發。

最后的 except 子句可以省略異常名,以用作通配符。但請謹慎使用,因為以這種方式很容易掩蓋真正的編程錯誤!它還可用于打印錯誤消息,然后重新引發異常(同樣允許調用者處理異常):

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except OSError as err:
    print("OS error: {0}".format(err))
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 OSError:
        print('cannot open', arg)
    else:
        print(arg, 'has', len(f.readlines()), 'lines')
        f.close()

使用 else 子句比向 try 子句添加額外的代碼要好,因為它避免了意外捕獲非 try ... except 語句保護的代碼引發的異常。

發生異常時,它可能具有關聯值,也稱為異常 參數 。參數的存在和類型取決于異常類型。

except 子句可以在異常名稱后面指定一個變量。這個變量和一個異常實例綁定,它的參數存儲在 instance.args 中。為了方便起見,異常實例定義了 __str__() ,因此可以直接打印參數而無需引用 .args 。也可以在拋出之前首先實例化異常,并根據需要向其添加任何屬性。:

>>> 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 be printed directly,
...                          # but may be overridden in exception subclasses
...     x, y = inst.args     # unpack args
...     print('x =', x)
...     print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs

如果異常有參數,則它們將作為未處理異常的消息的最后一部分('詳細信息')打印。

異常處理程序不僅處理 try 子句中遇到的異常,還處理 try 子句中調用(即使是間接地)的函數內部發生的異常。例如:

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

8.4. 拋出異常?

raise 語句允許程序員強制發生指定的異常。例如:

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

raise 唯一的參數就是要拋出的異常。這個參數必須是一個異常實例或者是一個異常類(派生自 Exception 的類)。如果傳遞的是一個異常類,它將通過調用沒有參數的構造函數來隱式實例化:

raise ValueError  # shorthand for 'raise ValueError()'

如果你需要確定是否引發了異常但不打算處理它,則可以使用更簡單的 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 <module>
NameError: HiThere

8.5. 用戶自定義異常?

程序可以通過創建新的異常類來命名它們自己的異常(有關Python 類的更多信息,請參閱 )。異常通常應該直接或間接地從 Exception 類派生。

可以定義異常類,它可以執行任何其他類可以執行的任何操作,但通常保持簡單,只提供一些屬性,這些屬性允許處理程序為異常提取有關錯誤的信息。 在創建可能引發多個不同錯誤的模塊時,通常的做法是為該模塊定義的異常創建基類,并為不同錯誤條件創建特定異常類的子類:

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

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

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

    def __init__(self, expression, message):
        self.expression = expression
        self.message = message

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

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

    def __init__(self, previous, next, message):
        self.previous = previous
        self.next = next
        self.message = message

大多數異常都定義為名稱以“Error”結尾,類似于標準異常的命名。

許多標準模塊定義了它們自己的異常,以報告它們定義的函數中可能出現的錯誤。有關類的更多信息,請參見 章節。

8.6. 定義清理操作?

try 語句有另一個可選子句,用于定義必須在所有情況下執行的清理操作。例如:

>>> try:
...     raise KeyboardInterrupt
... finally:
...     print('Goodbye, world!')
...
Goodbye, world!
KeyboardInterrupt
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>

如果存在 finally 子句,則 finally 子句將作為 try 語句結束前的最后一項任務被執行。 finally 子句不論 try 語句是否產生了異常都會被執行。 以下幾點討論了當異常發生時一些更復雜的情況:

  • 如果在執行 try 子句期間發生了異常,該異常可由一個 except 子句進行處理。 如果異常沒有被某個 except 子句所處理,則該異常會在 finally 子句執行之后被重新引發。

  • 異常也可能在 exceptelse 子句執行期間發生。 同樣地,該異常會在 finally 子句執行之后被重新引發。

  • 如果在執行 try 語句時遇到一個 break, continuereturn 語句,則 finally 子句將在執行 break, continuereturn 語句之前被執行。

  • 如果 finally 子句中包含一個 return 語句,則返回值將來自 finally 子句的某個 return 語句的返回值,而非來自 try 子句的 return 語句的返回值。

例如

>>> def bool_return():
...     try:
...         return True
...     finally:
...         return False
...
>>> bool_return()
False

一個更為復雜的例子:

>>> 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.0
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 <module>
  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, end="")

這個代碼的問題在于,它在這部分代碼執行完后,會使文件在一段不確定的時間內處于打開狀態。這在簡單腳本中不是問題,但對于較大的應用程序來說可能是個問題。 with 語句允許像文件這樣的對象能夠以一種確保它們得到及時和正確的清理的方式使用。:

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

執行完語句后,即使在處理行時遇到問題,文件 f 也始終會被關閉。和文件一樣,提供預定義清理操作的對象將在其文檔中指出這一點。