8. 錯(cuò)誤和異常?

至此,本教程還未深入介紹錯(cuò)誤信息,但如果您輸入過(guò)本教程前文中的例子,應(yīng)該已經(jīng)看到過(guò)一些錯(cuò)誤信息。目前,(至少)有兩種不同錯(cuò)誤:句法錯(cuò)誤異常。

8.1. 句法錯(cuò)誤?

句法錯(cuò)誤又稱解析錯(cuò)誤,是學(xué)習(xí) Python 時(shí)最常見(jiàn)的錯(cuò)誤:

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

解析器會(huì)復(fù)現(xiàn)出現(xiàn)句法錯(cuò)誤的代碼行,并用小“箭頭”指向行里檢測(cè)到的第一個(gè)錯(cuò)誤。錯(cuò)誤是由箭頭 上方 的 token 觸發(fā)的(至少是在這里檢測(cè)出的):本例中,在 print() 函數(shù)中檢測(cè)到錯(cuò)誤,因?yàn)?,在它前面缺少冒?hào)(':') 。錯(cuò)誤信息還輸出文件名與行號(hào),在使用腳本文件時(shí),就可以知道去哪里查錯(cuò)。

8.2. 異常?

即使語(yǔ)句或表達(dá)式使用了正確的語(yǔ)法,執(zhí)行時(shí)仍可能觸發(fā)錯(cuò)誤。執(zhí)行時(shí)檢測(cè)到的錯(cuò)誤稱為 異常,異常不一定導(dǎo)致嚴(yán)重的后果:很快我們就能學(xué)會(huì)如何處理 Python 的異常。大多數(shù)異常不會(huì)被程序處理,而是顯示下列錯(cuò)誤信息:

>>>
>>> 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 only concatenate str (not "int") to str

錯(cuò)誤信息的最后一行說(shuō)明程序遇到了什么類型的錯(cuò)誤。異常有不同的類型,而類型名稱會(huì)作為錯(cuò)誤信息的一部分中打印出來(lái):上述示例中的異常類型依次是:ZeroDivisionErrorNameErrorTypeError。作為異常類型打印的字符串是發(fā)生的內(nèi)置異常的名稱。對(duì)于所有內(nèi)置異常都是如此,但對(duì)于用戶定義的異常則不一定如此(雖然這種規(guī)范很有用)。標(biāo)準(zhǔn)的異常類型是內(nèi)置的標(biāo)識(shí)符(不是保留關(guān)鍵字)。

此行其余部分根據(jù)異常類型,結(jié)合出錯(cuò)原因,說(shuō)明錯(cuò)誤細(xì)節(jié)。

錯(cuò)誤信息開(kāi)頭用堆?;厮菪问秸故景l(fā)生異常的語(yǔ)境。一般會(huì)列出源代碼行的堆?;厮荩坏粫?huì)顯示從標(biāo)準(zhǔn)輸入讀取的行。

內(nèi)置異常 列出了內(nèi)置異常及其含義。

8.3. 異常的處理?

可以編寫程序處理選定的異常。下例會(huì)要求用戶一直輸入內(nèi)容,直到輸入有效的整數(shù),但允許用戶中斷程序(使用 Control-C 或操作系統(tǒng)支持的其他操作);注意,用戶中斷程序會(huì)觸發(fā) KeyboardInterrupt 異常。

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

try 語(yǔ)句的工作原理如下:

  • 首先,執(zhí)行 try 子句tryexcept 關(guān)鍵字之間的(多行)語(yǔ)句)。

  • 如果沒(méi)有觸發(fā)異常,則跳過(guò) except 子句,try 語(yǔ)句執(zhí)行完畢。

  • 如果在執(zhí)行 try 子句時(shí)發(fā)生了異常,則跳過(guò)該子句中剩下的部分。 如果異常的類型與 except 關(guān)鍵字后指定的異常相匹配,則會(huì)執(zhí)行 except 子句,然后跳到 try/except 代碼塊之后繼續(xù)執(zhí)行。

  • 如果發(fā)生的異常與 except 子句 中指定的異常不匹配,則它會(huì)被傳遞到外部的 try 語(yǔ)句中;如果沒(méi)有找到處理程序,則它是一個(gè) 未處理異常 且執(zhí)行將終止并輸出如上所示的消息。

try 語(yǔ)句可以有多個(gè) except 子句 來(lái)為不同的異常指定處理程序。 但最多只有一個(gè)處理程序會(huì)被執(zhí)行。 處理程序只處理對(duì)應(yīng)的 try 子句 中發(fā)生的異常,而不處理同一 try 語(yǔ)句內(nèi)其他處理程序中的異常。 except 子句 可以用帶圓括號(hào)的元組來(lái)指定多個(gè)異常,例如:

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

如果發(fā)生的異常與 except 子句中的類是同一個(gè)類或是它的基類時(shí),則該類與該異常相兼容(反之則不成立 --- 列出派生類的 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")

請(qǐng)注意如果顛倒 except 子句 的順序(把 except B 放在最前),則會(huì)輸出 B, B, B --- 即觸發(fā)了第一個(gè)匹配的 except 子句

When an exception occurs, it may have associated values, also known as the exception's arguments. The presence and types of the arguments depend on the exception type.

The except clause may specify a variable after the exception name. The variable is bound to the exception instance which typically has an args attribute that stores the arguments. For convenience, builtin exception types define __str__() to print all the arguments without explicitly accessing .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

The exception's __str__() output is printed as the last part ('detail') of the message for unhandled exceptions.

BaseException is the common base class of all exceptions. One of its subclasses, Exception, is the base class of all the non-fatal exceptions. Exceptions which are not subclasses of Exception are not typically handled, because they are used to indicate that the program should terminate. They include SystemExit which is raised by sys.exit() and KeyboardInterrupt which is raised when a user wishes to interrupt the program.

Exception can be used as a wildcard that catches (almost) everything. However, it is good practice to be as specific as possible with the types of exceptions that we intend to handle, and to allow any unexpected exceptions to propagate on.

The most common pattern for handling Exception is to print or log the exception and then re-raise it (allowing a caller to handle the exception as well):

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except OSError as err:
    print("OS error:", err)
except ValueError:
    print("Could not convert data to an integer.")
except Exception as err:
    print(f"Unexpected {err=}, {type(err)=}")
    raise

try ... except 語(yǔ)句具有可選的 else 子句,該子句如果存在,它必須放在所有 except 子句 之后。 它適用于 try 子句 沒(méi)有引發(fā)異常但又必須要執(zhí)行的代碼。 例如:

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 語(yǔ)句保護(hù)的代碼觸發(fā)的異常。

Exception handlers do not handle only exceptions that occur immediately in the try clause, but also those that occur inside functions that are called (even indirectly) in the try clause. For example:

>>>
>>> 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. 觸發(fā)異常?

raise 語(yǔ)句支持強(qiáng)制觸發(fā)指定的異常。例如:

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

The sole argument to raise indicates the exception to be raised. This must be either an exception instance or an exception class (a class that derives from BaseException, such as Exception or one of its subclasses). If an exception class is passed, it will be implicitly instantiated by calling its constructor with no arguments:

raise ValueError  # shorthand for 'raise ValueError()'

如果只想判斷是否觸發(fā)了異常,但并不打算處理該異常,則可以使用更簡(jiǎn)單的 raise 語(yǔ)句重新觸發(fā)異常:

>>>
>>> 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. 異常鏈?

raise 語(yǔ)句支持可選的 from 子句,該子句用于啟用鏈?zhǔn)疆惓!?例如:

# exc must be exception instance or None.
raise RuntimeError from exc

轉(zhuǎn)換異常時(shí),這種方式很有用。例如:

>>>
>>> def func():
...     raise ConnectionError
...
>>> try:
...     func()
... except ConnectionError as exc:
...     raise RuntimeError('Failed to open database') from exc
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "<stdin>", line 2, in func
ConnectionError

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

Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
RuntimeError: Failed to open database

異常鏈會(huì)在 exceptfinally 子句內(nèi)部引發(fā)異常時(shí)自動(dòng)生成。 這可以通過(guò)使用 from None 這樣的寫法來(lái)禁用:

>>>
>>> try:
...     open('database.sqlite')
... except OSError:
...     raise RuntimeError from None
...
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
RuntimeError

異常鏈機(jī)制詳見(jiàn) 內(nèi)置異常。

8.6. 用戶自定義異常?

程序可以通過(guò)創(chuàng)建新的異常類命名自己的異常(Python 類的內(nèi)容詳見(jiàn) )。不論是以直接還是間接的方式,異常都應(yīng)從 Exception 類派生。

Exception classes can be defined which do anything any other class can do, but are usually kept simple, often only offering a number of attributes that allow information about the error to be extracted by handlers for the exception.

大多數(shù)異常命名都以 “Error” 結(jié)尾,類似標(biāo)準(zhǔn)異常的命名。

Many standard modules define their own exceptions to report errors that may occur in functions they define.

8.7. 定義清理操作?

try 語(yǔ)句還有一個(gè)可選子句,用于定義在所有情況下都必須要執(zhí)行的清理操作。例如:

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

如果存在 finally 子句,則 finally 子句是 try 語(yǔ)句結(jié)束前執(zhí)行的最后一項(xiàng)任務(wù)。不論 try 語(yǔ)句是否觸發(fā)異常,都會(huì)執(zhí)行 finally 子句。以下內(nèi)容介紹了幾種比較復(fù)雜的觸發(fā)異常情景:

  • 如果執(zhí)行 try 子句期間觸發(fā)了某個(gè)異常,則某個(gè) except 子句應(yīng)處理該異常。如果該異常沒(méi)有 except 子句處理,在 finally 子句執(zhí)行后會(huì)被重新觸發(fā)。

  • exceptelse 子句執(zhí)行期間也會(huì)觸發(fā)異常。 同樣,該異常會(huì)在 finally 子句執(zhí)行之后被重新觸發(fā)。

  • 如果 finally 子句中包含 break、continuereturn 等語(yǔ)句,異常將不會(huì)被重新引發(fā)。

  • 如果執(zhí)行 try 語(yǔ)句時(shí)遇到 break,、continuereturn 語(yǔ)句,則 finally 子句在執(zhí)行 break、continuereturn 語(yǔ)句之前執(zhí)行。

  • 如果 finally 子句中包含 return 語(yǔ)句,則返回值來(lái)自 finally 子句的某個(gè) return 語(yǔ)句的返回值,而不是來(lái)自 try 子句的 return 語(yǔ)句的返回值。

例如:

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

這是一個(gè)比較復(fù)雜的例子:

>>>
>>> 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'

如上所示,任何情況下都會(huì)執(zhí)行 finally 子句。except 子句不處理兩個(gè)字符串相除觸發(fā)的 TypeError,因此會(huì)在 finally 子句執(zhí)行后被重新觸發(fā)。

在實(shí)際應(yīng)用程序中,finally 子句對(duì)于釋放外部資源(例如文件或者網(wǎng)絡(luò)連接)非常有用,無(wú)論是否成功使用資源。

8.8. 預(yù)定義的清理操作?

某些對(duì)象定義了不需要該對(duì)象時(shí)要執(zhí)行的標(biāo)準(zhǔn)清理操作。無(wú)論使用該對(duì)象的操作是否成功,都會(huì)執(zhí)行清理操作。比如,下例要打開(kāi)一個(gè)文件,并輸出文件內(nèi)容:

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

這個(gè)代碼的問(wèn)題在于,執(zhí)行完代碼后,文件在一段不確定的時(shí)間內(nèi)處于打開(kāi)狀態(tài)。在簡(jiǎn)單腳本中這沒(méi)有問(wèn)題,但對(duì)于較大的應(yīng)用程序來(lái)說(shuō)可能會(huì)出問(wèn)題。with 語(yǔ)句支持以及時(shí)、正確的清理的方式使用文件對(duì)象:

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

語(yǔ)句執(zhí)行完畢后,即使在處理行時(shí)遇到問(wèn)題,都會(huì)關(guān)閉文件 f。和文件一樣,支持預(yù)定義清理操作的對(duì)象會(huì)在文檔中指出這一點(diǎn)。

8.9. Raising and Handling Multiple Unrelated Exceptions?

There are situations where it is necessary to report several exceptions that have occurred. This it often the case in concurrency frameworks, when several tasks may have failed in parallel, but there are also other use cases where it is desirable to continue execution and collect multiple errors rather than raise the first exception.

The builtin ExceptionGroup wraps a list of exception instances so that they can be raised together. It is an exception itself, so it can be caught like any other exception.

>>>
>>> def f():
...     excs = [OSError('error 1'), SystemError('error 2')]
...     raise ExceptionGroup('there were problems', excs)
...
>>> f()
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 1, in <module>
  |   File "<stdin>", line 3, in f
  | ExceptionGroup: there were problems
  +-+---------------- 1 ----------------
    | OSError: error 1
    +---------------- 2 ----------------
    | SystemError: error 2
    +------------------------------------
>>> try:
...     f()
... except Exception as e:
...     print(f'caught {type(e)}: e')
...
caught <class 'ExceptionGroup'>: e
>>>

By using except* instead of except, we can selectively handle only the exceptions in the group that match a certain type. In the following example, which shows a nested exception group, each except* clause extracts from the group exceptions of a certain type while letting all other exceptions propagate to other clauses and eventually to be reraised.

>>>
>>> def f():
...     raise ExceptionGroup("group1",
...                          [OSError(1),
...                           SystemError(2),
...                           ExceptionGroup("group2",
...                                          [OSError(3), RecursionError(4)])])
...
>>> try:
...     f()
... except* OSError as e:
...     print("There were OSErrors")
... except* SystemError as e:
...     print("There were SystemErrors")
...
There were OSErrors
There were SystemErrors
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 2, in <module>
  |   File "<stdin>", line 2, in f
  | ExceptionGroup: group1
  +-+---------------- 1 ----------------
    | ExceptionGroup: group2
    +-+---------------- 1 ----------------
      | RecursionError: 4
      +------------------------------------
>>>

Note that the exceptions nested in an exception group must be instances, not types. This is because in practice the exceptions would typically be ones that have already been raised and caught by the program, along the following pattern:

>>>
>>> excs = []
... for test in tests:
...     try:
...         test.run()
...     except Exception as e:
...         excs.append(e)
...
>>> if excs:
...    raise ExceptionGroup("Test Failures", excs)
...

8.10. Enriching Exceptions with Notes?

When an exception is created in order to be raised, it is usually initialized with information that describes the error that has occurred. There are cases where it is useful to add information after the exception was caught. For this purpose, exceptions have a method add_note(note) that accepts a string and adds it to the exception's notes list. The standard traceback rendering includes all notes, in the order they were added, after the exception.

>>>
>>> try:
...     raise TypeError('bad type')
... except Exception as e:
...     e.add_note('Add some information')
...     e.add_note('Add some more information')
...     raise
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
TypeError: bad type
Add some information
Add some more information
>>>

For example, when collecting exceptions into an exception group, we may want to add context information for the individual errors. In the following each exception in the group has a note indicating when this error has occurred.

>>>
>>> def f():
...     raise OSError('operation failed')
...
>>> excs = []
>>> for i in range(3):
...     try:
...         f()
...     except Exception as e:
...         e.add_note(f'Happened in Iteration {i+1}')
...         excs.append(e)
...
>>> raise ExceptionGroup('We have some problems', excs)
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 1, in <module>
  | ExceptionGroup: We have some problems (3 sub-exceptions)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |   File "<stdin>", line 2, in f
    | OSError: operation failed
    | Happened in Iteration 1
    +---------------- 2 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |   File "<stdin>", line 2, in f
    | OSError: operation failed
    | Happened in Iteration 2
    +---------------- 3 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |   File "<stdin>", line 2, in f
    | OSError: operation failed
    | Happened in Iteration 3
    +------------------------------------
>>>