8. 錯(cuò)誤和異常?
到目前為止,我們還沒(méi)有提到錯(cuò)誤消息,但是如果你已經(jīng)嘗試過(guò)那些例子,你可能已經(jīng)看過(guò)了一些錯(cuò)誤消息。 目前(至少)有兩種可區(qū)分的錯(cuò)誤:語(yǔ)法錯(cuò)誤 和 異常。
8.1. 語(yǔ)法錯(cuò)誤?
語(yǔ)法錯(cuò)誤又稱(chēng)解析錯(cuò)誤,可能是你在學(xué)習(xí)Python 時(shí)最容易遇到的錯(cuò)誤:
>>> while True print('Hello world')
File "<stdin>", line 1
while True print('Hello world')
^
SyntaxError: invalid syntax
解析器會(huì)輸出出現(xiàn)語(yǔ)法錯(cuò)誤的那一行,并顯示一個(gè)“箭頭”,指向這行里面檢測(cè)到的第一個(gè)錯(cuò)誤。 錯(cuò)誤是由箭頭指示的位置 上面 的 token 引起的(或者至少是在這里被檢測(cè)出的):在示例中,在 print() 這個(gè)函數(shù)中檢測(cè)到了錯(cuò)誤,因?yàn)樵谒懊嫔倭藗€(gè)冒號(hào) (':') 。文件名和行號(hào)也會(huì)被輸出,以便輸入來(lái)自腳本文件時(shí)你能知道去哪檢查。
8.2. 異常?
即使語(yǔ)句或表達(dá)式在語(yǔ)法上是正確的,但在嘗試執(zhí)行時(shí),它仍可能會(huì)引發(fā)錯(cuò)誤。 在執(zhí)行時(shí)檢測(cè)到的錯(cuò)誤被稱(chēng)為 異常,異常不一定會(huì)導(dǎo)致嚴(yán)重后果:你將很快學(xué)會(huì)如何在 Python 程序中處理它們。 但是,大多數(shù)異常并不會(huì)被程序處理,此時(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't convert 'int' object to str implicitly
錯(cuò)誤信息的最后一行告訴我們程序遇到了什么類(lèi)型的錯(cuò)誤。異常有不同的類(lèi)型,而其類(lèi)型名稱(chēng)將會(huì)作為錯(cuò)誤信息的一部分中打印出來(lái):上述示例中的異常類(lèi)型依次是:ZeroDivisionError, NameError 和 TypeError。作為異常類(lèi)型打印的字符串是發(fā)生的內(nèi)置異常的名稱(chēng)。對(duì)于所有內(nèi)置異常都是如此,但對(duì)于用戶(hù)定義的異常則不一定如此(雖然這是一個(gè)有用的規(guī)范)。標(biāo)準(zhǔn)的異常類(lèi)型是內(nèi)置的標(biāo)識(shí)符(而不是保留關(guān)鍵字)。
這一行的剩下的部分根據(jù)異常類(lèi)型及其原因提供詳細(xì)信息。
錯(cuò)誤信息的前一部分以堆?;厮莸男问斤@示發(fā)生異常時(shí)的上下文。通常它包含列出源代碼行的堆?;厮荩坏撬粫?huì)顯示從標(biāo)準(zhǔn)輸入中讀取的行。
內(nèi)置異常 列出了內(nèi)置異常和它們的含義。
8.3. 處理異常?
可以編寫(xiě)處理所選異常的程序。請(qǐng)看下面的例子,它會(huì)要求用戶(hù)一直輸入,直到輸入的是一個(gè)有效的整數(shù),但允許用戶(hù)中斷程序(使用 Control-C 或操作系統(tǒng)支持的其他操作);請(qǐng)注意用戶(hù)引起的中斷可以通過(guò)引發(fā) KeyboardInterrupt 異常來(lái)指示。:
>>> 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ǔ)句的執(zhí)行。如果在執(zhí)行 try 子句時(shí)發(fā)生了異常,則跳過(guò)該子句中剩下的部分。 然后,如果異常的類(lèi)型和
except關(guān)鍵字后面的異常匹配,則執(zhí)行 except 子句,然后繼續(xù)執(zhí)行try語(yǔ)句之后的代碼。如果發(fā)生的異常和 except 子句中指定的異常不匹配,則將其傳遞到外部的
try語(yǔ)句中;如果沒(méi)有找到處理程序,則它是一個(gè) 未處理異常,執(zhí)行將停止并顯示如上所示的消息。
一個(gè) try 語(yǔ)句可能有多個(gè) except 子句,以指定不同異常的處理程序。 最多會(huì)執(zhí)行一個(gè)處理程序。 處理程序只處理相應(yīng)的 try 子句中發(fā)生的異常,而不處理同一 try 語(yǔ)句內(nèi)其他處理程序中的異常。 一個(gè) except 子句可以將多個(gè)異常命名為帶括號(hào)的元組,例如:
... except (RuntimeError, TypeError, NameError):
... pass
如果發(fā)生的異常和 except 子句中的類(lèi)是同一個(gè)類(lèi)或者是它的基類(lèi),則異常和 except 子句中的類(lèi)是兼容的(但反過(guò)來(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")
請(qǐng)注意如果 except 子句被顛倒(把 except B 放到第一個(gè)),它將打印 B,B,B --- 即第一個(gè)匹配的 except 子句被觸發(fā)。
最后的 except 子句可以省略異常名,以用作通配符。但請(qǐng)謹(jǐn)慎使用,因?yàn)橐赃@種方式很容易掩蓋真正的編程錯(cuò)誤!它還可用于打印錯(cuò)誤消息,然后重新引發(fā)異常(同樣允許調(diào)用者處理異常):
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 語(yǔ)句有一個(gè)可選的 else 子句,在使用時(shí)必須放在所有的 except 子句后面。對(duì)于在 try 子句不引發(fā)異常時(shí)必須執(zhí)行的代碼來(lái)說(shuō)很有用。 例如:
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 子句添加額外的代碼要好,因?yàn)樗苊饬艘馔獠东@非 try ... except 語(yǔ)句保護(hù)的代碼引發(fā)的異常。
發(fā)生異常時(shí),它可能具有關(guān)聯(lián)值,也稱(chēng)為異常 參數(shù) 。參數(shù)的存在和類(lèi)型取決于異常類(lèi)型。
except 子句可以在異常名稱(chēng)后面指定一個(gè)變量。這個(gè)變量和一個(gè)異常實(shí)例綁定,它的參數(shù)存儲(chǔ)在 instance.args 中。為了方便起見(jiàn),異常實(shí)例定義了 __str__() ,因此可以直接打印參數(shù)而無(wú)需引用 .args 。也可以在拋出之前首先實(shí)例化異常,并根據(jù)需要向其添加任何屬性。:
>>> 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
如果異常有參數(shù),則它們將作為未處理異常的消息的最后一部分('詳細(xì)信息')打印。
異常處理程序不僅處理 try 子句中遇到的異常,還處理 try 子句中調(diào)用(即使是間接地)的函數(shù)內(nèi)部發(fā)生的異常。例如:
>>> 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 語(yǔ)句允許程序員強(qiáng)制發(fā)生指定的異常。例如:
>>> raise NameError('HiThere')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: HiThere
raise 唯一的參數(shù)就是要拋出的異常。這個(gè)參數(shù)必須是一個(gè)異常實(shí)例或者是一個(gè)異常類(lèi)(派生自 Exception 的類(lèi))。如果傳遞的是一個(gè)異常類(lèi),它將通過(guò)調(diào)用沒(méi)有參數(shù)的構(gòu)造函數(shù)來(lái)隱式實(shí)例化:
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. 用戶(hù)自定義異常?
程序可以通過(guò)創(chuàng)建新的異常類(lèi)來(lái)命名它們自己的異常(有關(guān)Python 類(lèi)的更多信息,請(qǐng)參閱 類(lèi))。異常通常應(yīng)該直接或間接地從 Exception 類(lèi)派生。
可以定義異常類(lèi),它可以執(zhí)行任何其他類(lèi)可以執(zhí)行的任何操作,但通常保持簡(jiǎn)單,只提供一些屬性,這些屬性允許處理程序?yàn)楫惓L崛∮嘘P(guān)錯(cuò)誤的信息。 在創(chuàng)建可能引發(fā)多個(gè)不同錯(cuò)誤的模塊時(shí),通常的做法是為該模塊定義的異常創(chuàng)建基類(lèi),并為不同錯(cuò)誤條件創(chuàng)建特定異常類(lèi)的子類(lèi):
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
大多數(shù)異常都定義為名稱(chēng)以“Error”結(jié)尾,類(lèi)似于標(biāo)準(zhǔn)異常的命名。
許多標(biāo)準(zhǔn)模塊定義了它們自己的異常,以報(bào)告它們定義的函數(shù)中可能出現(xiàn)的錯(cuò)誤。有關(guān)類(lèi)的更多信息,請(qǐng)參見(jiàn) 類(lèi) 章節(jié)。
8.6. 定義清理操作?
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é)束前的最后一項(xiàng)任務(wù)被執(zhí)行。 finally 子句不論 try 語(yǔ)句是否產(chǎn)生了異常都會(huì)被執(zhí)行。 以下幾點(diǎn)討論了當(dāng)異常發(fā)生時(shí)一些更復(fù)雜的情況:
如果在執(zhí)行
try子句期間發(fā)生了異常,該異??捎梢粋€(gè)except子句進(jìn)行處理。 如果異常沒(méi)有被某個(gè)except子句所處理,則該異常會(huì)在finally子句執(zhí)行之后被重新引發(fā)。異常也可能在
except或else子句執(zhí)行期間發(fā)生。 同樣地,該異常會(huì)在finally子句執(zhí)行之后被重新引發(fā)。如果在執(zhí)行
try語(yǔ)句時(shí)遇到一個(gè)break,continue或return語(yǔ)句,則finally子句將在執(zhí)行break,continue或return語(yǔ)句之前被執(zhí)行。如果
finally子句中包含一個(gè)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'
正如你所看到的,finally 子句在任何情況下都會(huì)被執(zhí)行。 兩個(gè)字符串相除所引發(fā)的 TypeError 不會(huì)由 except 子句處理,因此會(huì)在 finally 子句執(zhí)行后被重新引發(fā)。
在實(shí)際應(yīng)用程序中,finally 子句對(duì)于釋放外部資源(例如文件或者網(wǎng)絡(luò)連接)非常有用,無(wú)論是否成功使用資源。
8.7. 預(yù)定義的清理操作?
某些對(duì)象定義了在不再需要該對(duì)象時(shí)要執(zhí)行的標(biāo)準(zhǔn)清理操作,無(wú)論使用該對(duì)象的操作是成功還是失敗,清理操作都會(huì)被執(zhí)行。 請(qǐng)查看下面的示例,它嘗試打開(kāi)一個(gè)文件并把其內(nèi)容打印到屏幕上。:
for line in open("myfile.txt"):
print(line, end="")
這個(gè)代碼的問(wèn)題在于,它在這部分代碼執(zhí)行完后,會(huì)使文件在一段不確定的時(shí)間內(nèi)處于打開(kāi)狀態(tài)。這在簡(jiǎn)單腳本中不是問(wèn)題,但對(duì)于較大的應(yīng)用程序來(lái)說(shuō)可能是個(gè)問(wèn)題。 with 語(yǔ)句允許像文件這樣的對(duì)象能夠以一種確保它們得到及時(shí)和正確的清理的方式使用。:
with open("myfile.txt") as f:
for line in f:
print(line, end="")
執(zhí)行完語(yǔ)句后,即使在處理行時(shí)遇到問(wèn)題,文件 f 也始終會(huì)被關(guān)閉。和文件一樣,提供預(yù)定義清理操作的對(duì)象將在其文檔中指出這一點(diǎn)。
