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