8. 錯誤和異常?
至此,本教程還未深入介紹錯誤信息,但如果您輸入過本教程前文中的例子,應該已經(jīng)看到過一些錯誤信息。目前,(至少)有兩種不同錯誤:句法錯誤 和 異常。
8.1. 句法錯誤?
句法錯誤又稱解析錯誤,是學習 Python 時最常見的錯誤:
>>> while True print('Hello world')
File "<stdin>", line 1
while True print('Hello world')
^
SyntaxError: invalid syntax
解析器會復現(xiàn)出現(xiàn)句法錯誤的代碼行,并用小“箭頭”指向行里檢測到的第一個錯誤。錯誤是由箭頭 上方 的 token 觸發(fā)的(至少是在這里檢測出的):本例中,在 print()
函數(shù)中檢測到錯誤,因為,在它前面缺少冒號(':'
) 。錯誤信息還輸出文件名與行號,在使用腳本文件時,就可以知道去哪里查錯。
8.2. 異常?
即使語句或表達式使用了正確的語法,執(zhí)行時仍可能觸發(fā)錯誤。執(zhí)行時檢測到的錯誤稱為 異常,異常不一定導致嚴重的后果:很快我們就能學會如何處理 Python 的異常。大多數(shù)異常不會被程序處理,而是顯示下列錯誤信息:
>>> 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
錯誤信息的最后一行說明程序遇到了什么類型的錯誤。異常有不同的類型,而類型名稱會作為錯誤信息的一部分中打印出來:上述示例中的異常類型依次是:ZeroDivisionError
, NameError
和 TypeError
。作為異常類型打印的字符串是發(fā)生的內(nèi)置異常的名稱。對于所有內(nèi)置異常都是如此,但對于用戶定義的異常則不一定如此(雖然這種規(guī)范很有用)。標準的異常類型是內(nèi)置的標識符(不是保留關鍵字)。
此行其余部分根據(jù)異常類型,結合出錯原因,說明錯誤細節(jié)。
錯誤信息開頭用堆?;厮菪问秸故景l(fā)生異常的語境。一般會列出源代碼行的堆棧回溯;但不會顯示從標準輸入讀取的行。
內(nèi)置異常 列出了內(nèi)置異常及其含義。
8.3. 異常的處理?
可以編寫程序處理選定的異常。下例會要求用戶一直輸入內(nèi)容,直到輸入有效的整數(shù),但允許用戶中斷程序(使用 Control-C 或操作系統(tǒng)支持的其他操作);注意,用戶中斷程序會觸發(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
語句的工作原理如下:
如果沒有觸發(fā)異常,則跳過 except 子句,
try
語句執(zhí)行完畢。如果在執(zhí)行
try
子句時發(fā)生了異常,則跳過該子句中剩下的部分。 如果異常的類型與except
關鍵字后指定的異常相匹配,則會執(zhí)行 except 子句,然后跳到 try/except 代碼塊之后繼續(xù)執(zhí)行。如果發(fā)生的異常與 except 子句 中指定的異常不匹配,則它會被傳遞到外部的
try
語句中;如果沒有找到處理程序,則它是一個 未處理異常 且執(zhí)行將終止并輸出如上所示的消息。
try
語句可以有多個 except 子句 來為不同的異常指定處理程序。 但最多只有一個處理程序會被執(zhí)行。 處理程序只處理對應的 try 子句 中發(fā)生的異常,而不處理同一 try
語句內(nèi)其他處理程序中的異常。 except 子句 可以用帶圓括號的元組來指定多個異常,例如:
... except (RuntimeError, TypeError, NameError):
... pass
如果發(fā)生的異常與 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 --- 即觸發(fā)了第一個匹配的 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
語句具有可選的 else 子句,該子句如果存在,它必須放在所有 except 子句 之后。 它適用于 try 子句 沒有引發(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
語句保護的代碼觸發(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
語句支持強制觸發(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ā)了異常,但并不打算處理該異常,則可以使用更簡單的 raise
語句重新觸發(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
語句支持可選的 from
子句,該子句用于啟用鏈式異常。 例如:
# exc must be exception instance or None.
raise RuntimeError from exc
轉換異常時,這種方式很有用。例如:
>>> 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
異常鏈會在 except
或 finally
子句內(nèi)部引發(fā)異常時自動生成。 這可以通過使用 from None
這樣的寫法來禁用:
>>> try:
... open('database.sqlite')
... except OSError:
... raise RuntimeError from None
...
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
RuntimeError
異常鏈機制詳見 內(nèi)置異常。
8.6. 用戶自定義異常?
程序可以通過創(chuàng)建新的異常類命名自己的異常(Python 類的內(nèi)容詳見 類)。不論是以直接還是間接的方式,異常都應從 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” 結尾,類似標準異常的命名。
Many standard modules define their own exceptions to report errors that may occur in functions they define.
8.7. 定義清理操作?
try
語句還有一個可選子句,用于定義在所有情況下都必須要執(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
語句結束前執(zhí)行的最后一項任務。不論 try
語句是否觸發(fā)異常,都會執(zhí)行 finally
子句。以下內(nèi)容介紹了幾種比較復雜的觸發(fā)異常情景:
如果執(zhí)行
try
子句期間觸發(fā)了某個異常,則某個except
子句應處理該異常。如果該異常沒有except
子句處理,在finally
子句執(zhí)行后會被重新觸發(fā)。except
或else
子句執(zhí)行期間也會觸發(fā)異常。 同樣,該異常會在finally
子句執(zhí)行之后被重新觸發(fā)。如果
finally
子句中包含break
、continue
或return
等語句,異常將不會被重新引發(fā)。如果執(zhí)行
try
語句時遇到break
,、continue
或return
語句,則finally
子句在執(zhí)行break
、continue
或return
語句之前執(zhí)行。如果
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'
如上所示,任何情況下都會執(zhí)行 finally
子句。except
子句不處理兩個字符串相除觸發(fā)的 TypeError
,因此會在 finally
子句執(zhí)行后被重新觸發(fā)。
在實際應用程序中,finally
子句對于釋放外部資源(例如文件或者網(wǎng)絡連接)非常有用,無論是否成功使用資源。
8.8. 預定義的清理操作?
某些對象定義了不需要該對象時要執(zhí)行的標準清理操作。無論使用該對象的操作是否成功,都會執(zhí)行清理操作。比如,下例要打開一個文件,并輸出文件內(nèi)容:
for line in open("myfile.txt"):
print(line, end="")
這個代碼的問題在于,執(zhí)行完代碼后,文件在一段不確定的時間內(nèi)處于打開狀態(tài)。在簡單腳本中這沒有問題,但對于較大的應用程序來說可能會出問題。with
語句支持以及時、正確的清理的方式使用文件對象:
with open("myfile.txt") as f:
for line in f:
print(line, end="")
語句執(zhí)行完畢后,即使在處理行時遇到問題,都會關閉文件 f。和文件一樣,支持預定義清理操作的對象會在文檔中指出這一點。
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
+------------------------------------
>>>