目前在 Python
中(至少)有兩種可區分的錯誤:語法錯誤和異常。html
語法錯誤又稱解析錯誤,多是在學習 Python 時最容易遇到的錯誤:python
>>> while True print('Hello world')
File "<stdin>", line 1
while True print('Hello world')
^
SyntaxError: invalid syntax
複製代碼
在執行時檢測到的錯誤被稱爲異常,大多數異常並不會被程序自動處理,此時會顯示以下所示的錯誤信息:程序員
>>> 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
複製代碼
錯誤信息的最後一行告訴咱們程序遇到了什麼類型的錯誤。異常有不一樣的類型,而其類型名稱將會做爲錯誤信息的一部分中打印出來。這一行的剩下的部分根據異常類型及其緣由提供詳細信息。編程
錯誤信息的前一部分以堆棧回溯的形式顯示發生異常時的上下文。一般它包含列出源代碼行的堆棧回溯;可是它不會顯示從標準輸入中讀取的行。api
做爲異常類型打印的字符串是發生的內置異常的名稱。對於全部內置異常都是如此,但對於用戶定義的異常則不必定如此(雖然這是一個有用的規範)。標準的異常類型是內置的標識符(而不是保留關鍵字)。bash
篇幅問題,請參考:Python 中的內置異常網絡
異常處理工做由「捕獲」和「拋出」兩部分組成。「捕獲」指的是使用 try ... except
包裹特定語句,穩當的完成錯誤流程處理。而恰當的使用 raise
主動「拋出」異常,更是優雅代碼裏必不可少的組成部分。函數
try
子句(try
和 except
關鍵字之間的(多行)語句)。except
子句並完成 try
語句的執行。try
子句時發生了異常,則跳過該子句中剩下的部分。而後,若是異常的類型和 except
關鍵字後面的異常匹配,則執行 except
子句 ,而後繼續執行 try
語句以後的代碼。except
子句中指定的異常不匹配,則將其傳遞到外部的 try
語句中;若是沒有找處處理程序,則它是一個未處理異常,執行將中止並顯示錯誤的消息。一個 try 語句可能有多個 except 子句,以指定不一樣異常的處理程序,但最多會執行一個處理程序。處理程序只處理相應的 try
子句中發生的異常,而不處理同一 try
語句內其餘處理程序中的異常。一個 except
子句能夠將多個異常命名爲帶括號的元組,例如:學習
... except (RuntimeError, TypeError, NameError):
... pass
複製代碼
若是發生的異常和 except
子句中的類是同一個類或者是它的基類,則異常和 except
子句中的類是兼容的(但反過來則不成立)。例如,下面的代碼將依次打印 B, C, D
ui
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
子句能夠省略異常名,以用做通配符。但請謹慎使用,由於以這種方式很容易掩蓋真正的編程錯誤!它還可用於打印錯誤消息,而後從新引起異常(一樣容許調用者處理異常)。
try ... except
語句有一個可選的 else
子句,在使用時必須放在全部的 except
子句後面。對於在 try
子句不引起異常時必須執行的代碼來講頗有用。
使用 else
子句比向 try
子句添加額外的代碼要好,由於它避免了意外捕獲由 try ... except
語句保護的代碼未引起的異常。
異常處理程序不只處理 try
子句中遇到的異常,還處理 try
子句中調用(即便是間接地)的函數內部發生的異常。
發生異常時,它可能具備關聯值,也稱爲異常參數。參數的存在和類型取決於異常類型。
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
複製代碼
若是異常有參數,則它們將做爲未處理異常的消息的最後一部分(詳細信息)打印。
raise
語句容許程序員強制發生指定的異常。例如:
>>> raise NameError('HiThere')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: HiThere
複製代碼
raise
惟一的參數就是要拋出的異常。這個參數必須是一個異常實例或者是一個異常類(派生自 Exception
的類)。若是傳遞的是一個異常類,它將經過調用沒有參數的構造函數來隱式實例化:
raise ValueError # 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
複製代碼
程序能夠經過建立新的異常類來命名它們本身的異常。異常一般應該直接或間接地從 Exception
類派生。
能夠定義異常類,它能夠執行任何其餘類能夠執行的任何操做,但一般保持簡單,一般只提供許多屬性,這些屬性容許處理程序爲異常提取有關錯誤的信息。在建立可能引起多個不一樣錯誤的模塊時,一般的作法是爲該模塊定義的異常建立基類,併爲不一樣錯誤條件建立特定異常類的子類。
大多數異常都定義爲名稱以 Error
結尾,相似於標準異常的命名。
許多標準模塊定義了它們本身的異常,以報告它們定義的函數中可能出現的錯誤。
try
語句有另外一個可選子句,用於定義必須在全部狀況下執行的清理操做。
finally
子句總會在離開 try
語句前被執行,不管是否發生了異常。當在 try
子句中發生了異常且還沒有被 except
子句處理(或者它發生在 except
或 else
子句中)時,它將在 finally
子句執行後被從新拋出。當 try
語句的任何其餘子句經過 break, continue, return
語句離開時,finally
也會在「離開以前」被執行。
在實際應用程序中,finally
子句對於**釋放外部資源(例如文件或者網絡鏈接)**很是有用,不管是否成功使用資源。
有時咱們會在捕捉到一個異常後從新引起它(傳遞異常),實現起來很簡單,使用不帶參數的 raise
語句便可,例如:
def f1():
print(1/0)
def f2():
try:
f1()
except Exception as e:
print('something worng')
raise
f2()
複製代碼
# 運行結果
something worng
Traceback (most recent call last):
File "/Users/ryoma/Desktop/project/learn/learn_python/python_exception.py", line 11, in <module>
f2()
File "/Users/ryoma/Desktop/project/learn/learn_python/python_exception.py", line 6, in f2
f1()
File "/Users/ryoma/Desktop/project/learn/learn_python/python_exception.py", line 2, in f1
print(1/0)
ZeroDivisionError: division by zero
複製代碼
Python
自己提供了不少語法範式簡化了異常處理,例如:
for
語句利用 Stoplteration
異常來結束循環的with
語句在打開文件後會在操做結束後(不管是否正常結束)會自動關閉文件句柄getattr()
函數獲取對象中的不肯定屬性以上這些都是 Python
自身封裝好的語法範式,在處理這些事件的時候應避免使用 try/except/finally
的思惟來處理。
在 Python
中使用異常捕獲的目的並非使本身寫的代碼不出現任何異常,而是在可能因外部力量而出錯的部分進行預防,例如對用戶輸入部分進行異常捕獲。
在 Python
中使用異常捕獲時應捕獲儘量精確的異常類型,而不是模糊的 Exception
,由於模糊的捕獲 Exception
有時會致使本該被顯示的有用的錯誤信息被自定義的錯誤信息「吃」掉。
另外,使本身寫的代碼不出現任何異常的最好方法是規範的代碼書寫習慣。
不少場景下咱們會對異常類進行包裝,方便在產生已知異常時自定義錯誤信息,這樣作能大大提升後續的編碼效率,但在使用時若是沒有作好分層處理很容易擊穿代碼的抽象分層邏輯,具體案例請參考 Python 工匠: 異常處理的三個好習慣。
爲了不由於使用錯誤的異常處理方式致使代碼的抽象分層邏輯被打破:
當非異常處理邏輯代碼中存在大量異常處理操做時,很容易出現因異常處理的邏輯代碼太多而擾亂核心的邏輯代碼。
# 代碼來自:Python 工匠:異常處理的三個好習慣
def upload_avatar(request):
"""用戶上傳新頭像"""
try:
avatar_file = request.FILES['avatar']
except KeyError:
raise error_codes.AVATAR_FILE_NOT_PROVIDED
try:
resized_avatar_file = resize_avatar(avatar_file)
except FileTooLargeError as e:
raise error_codes.AVATAR_FILE_TOO_LARGE
except ResizeAvatarError as e:
raise error_codes.AVATAR_FILE_INVALID
try:
request.user.avatar = resized_avatar_file
request.user.save()
except Exception:
raise error_codes.INTERNAL_SERVER_ERROR
return HttpResponse({})
複製代碼
此時咱們可使用 Python
中的 **上下文管理器(context manager)**配合 with
語句簡化異常處理過程。
# 代碼來自:Python 工匠:異常處理的三個好習慣
class raise_api_error:
"""captures specified exception and raise ApiErrorCode instead :raises: AttributeError if code_name is not valid """
def __init__(self, captures, code_name):
self.captures = captures
self.code = getattr(error_codes, code_name)
def __enter__(self):
# 剛方法將在進入上下文時調用
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# 該方法將在退出上下文時調用
# exc_type, exc_val, exc_tb 分別表示該上下文內拋出的
# 異常類型、異常值、錯誤棧
if exc_type is None:
return False
if exc_type == self.captures:
raise self.code from exc_val
return False
複製代碼
在上面的代碼裏,定義了一個名爲 raise_api_error
的上下文管理器,它在進入上下文時什麼也不作。可是在退出上下文時,會判斷當前上下文中是否拋出了類型爲 self.captures
的異常,若是有,就用 APIErrorCode
異常類替代它。
使用該上下文管理器後,上面臃腫的 upload_avatar
函數變得更清晰簡潔:
# 代碼來自:Python 工匠:異常處理的三個好習慣
def upload_avatar(request):
"""用戶上傳新頭像"""
with raise_api_error(KeyError, 'AVATAR_FILE_NOT_PROVIDED'):
avatar_file = request.FILES['avatar']
with raise_api_error(ResizeAvatarError, 'AVATAR_FILE_INVALID'),\
raise_api_error(FileTooLargeError, 'AVATAR_FILE_TOO_LARGE'):
resized_avatar_file = resize_avatar(avatar_file)
with raise_api_error(Exception, 'INTERNAL_SERVER_ERROR'):
request.user.avatar = resized_avatar_file
request.user.save()
return HttpResponse({})
複製代碼
感謝參考文章的做者(譯者)