理解Python Traceback(回溯)

當一個異常在你的代碼中被引發時,Python會打印一個traceback(回溯)。如果你是第一次看到回溯輸出,或者你不知道它在告訴你什麼,那麼它可能會讓你不知所措。但是Python回溯具有豐富的信息,可以幫助你診斷和修復代碼中引發異常的原因。理解Python回溯提供了什麼信息對於成為一個更好的Python程序員至關重要。

在本教程結束時,你將能夠:

  • 理解你下一次遇到的回溯
  • 識別一些很常見的回溯
  • 在處理異常的同時成功記錄回溯

什麼是Python回溯?

回溯是一個報告,其中包含在你的代碼中某個特定點上執行的函數調用。回溯有很多名稱,包括堆棧跟蹤、堆棧回溯、向後追溯,也許還有其他名稱。在Python中,使用的術語是回溯。

當你的程序引發一個異常時,Python將打印當前回溯信息以幫助你知道哪裡出錯了。下面是一個例子來說明這種情況:

理解Python Traceback(回溯)

在這裡,我們使用參數someone調用greet()。但是,在greet()中,這個變量名沒有被使用。相反,它在print()調用中被錯誤拼寫為someon。

注意:本教程假設你理解Python異常。如果你不熟悉或者只是想複習一下,那麼你應該查看《Python異常:介紹》。

當你運行這個程序時,你會得到以下回溯:

理解Python Traceback(回溯)

此回溯輸出包含你診斷問題所需的所有信息。回溯輸出的最後一行告訴你引發的是什麼類型的異常,以及關於此異常的一些相關信息。回溯的前幾行指出了引發異常的代碼。

在上面的回溯中,該異常是一個NameError,這意味著有一個對未定義的名稱(變量、函數、類)的引用。在本例中,被引用的名稱是someon。

本例中的最後一行有足夠的信息來幫助你解決問題。在代碼中搜索名稱someon時(這是一個拼寫錯誤)將為你指明正確的方向。然而,你的代碼通常要比這個例子複雜得多。

如何閱讀Python回溯?

當你試圖確定代碼中引發異常的原因時,Python回溯中包含許多有用的信息。在本節中,你將瀏覽不同的回溯,以便理解回溯中包含的不同信息。

Python回溯概述

每個Python回溯都有幾個重要的部分。下圖突出顯示了各個部分:

理解Python Traceback(回溯)

在Python中,最好從底部往上閱讀回溯:

  1. 藍色框: 回溯的最後一行是錯誤消息行。它包含捕獲的異常名稱。
  2. 綠色框: 異常名稱之後是錯誤消息。此消息通常包含有助於理解引發異常的原因的信息。
  3. 黃色框: 在回溯的上端,各種函數調用從底部移動到頂部,從最近的函數調用到最遠的函數調用。這些調用的每一個調用都由兩行條目表示。每個調用的第一行包含文件名、行號和模塊名等信息,這些信息都指定了代碼的位置。
  4. 紅色下劃線: 這些調用的第二行包含實際被執行的代碼。

當你在命令行中執行代碼和在REPL中運行代碼時,回溯輸出會有一些不同。下面是在REPL中執行的與上一節相同的代碼以及執行後產生的回溯輸出:

理解Python Traceback(回溯)

注意用 "<stdin>"替代文件名的地方。這是有意義的,因為你是通過標準輸入來輸入代碼的。而且,執行的代碼行不會顯示在回溯中。/<stdin>

注意: 如果你經常在其他編程語言中查看回溯,那麼你會注意到它與Python的回溯方式相比有一個主要不同。大多數其他語言在頂部打印異常,然後從頂部到底部,從最近的調用到最遠的調用。

前面已經說過,但這裡還是要重申一下,你應該從底部到頂部來閱讀Python回溯。這是非常有用的,因為回溯會被打印出來,而你的終端(或你正在閱讀回溯的任何地方)通常會在輸出的底部結束,這為你提供了開始閱讀回溯的最佳位置。

具體的回溯瀏覽

瀏覽一些具體的回溯輸出,可以幫助你更好地理解並查看回溯將為你提供什麼信息。

下面代碼被用在例子中來說明Python回溯為你提供的信息:

理解Python Traceback(回溯)

在這裡,who_to_greet()接受一個值person,並返回它或提示輸入一個值返回來代替。

然後,greet()接受一個要打招呼的名字someone和一個可選的greeting值,並傳入someone值來調用print(). who_to_greet()。

最後,greet_many()將遍歷people列表並調用greet()。如果調用greet()時引發異常,則打印出一個簡單的備份問候語。

只要你提供了正確的輸入,這段代碼沒有任何bug會引發異常。

如果你在greetings.py的底部添加一個greet()調用,然後指定一個它無法預期的關鍵字參數(例如,greet('Chad', greting='Yo')),那你將得到以下回溯:

理解Python Traceback(回溯)

同樣,對於一個Python回溯,最好是向後處理,向上移動輸出。從回溯的最後一行開始,你可以看到異常是一個TypeError。異常類型後面的消息(冒號後面的所有內容)為你提供了一些很好的信息。它告訴你,greet()被調用時帶有一個它沒有預料到的關鍵字參數,併為你提供了未知參數的名稱:greting。

繼續向上移動,你可以看到導致異常的行。在本例中,這個行就是我們在greetings .py的底部添加的greet()調用。

向上的下一行給出了代碼所在文件的路徑、代碼所在文件的行號以及代碼所在的模塊。在本例中,因為我們的代碼沒有使用任何其他Python模塊,所以這裡我們只會看到<module>,這意味著這就是正在被執行的文件。/<module>

使用不同的文件和不同的輸入,你可以看到回溯實際上會向你指出正確的方向來找到問題。如果你正在進行跟蹤,請從greetings.py底部刪除有問題的greet()調用,並將以下文件添加到你的目錄中:

理解Python Traceback(回溯)

這裡,你已經設置了另一個Python文件,該文件將導入前面的模塊greetings.py,並從中使用greet()。下面是運行example.py時會發生的事情:

理解Python Traceback(回溯)

在本例中捕獲的異常同樣是一個TypeError,但這一次消息的幫助要小一些。它告訴你,在代碼的某個地方,它期望使用一個字符串,但是傳入了一個整數。

向上移動,你可以看到被執行的代碼行。然後是文件和代碼的行號。不過,這一次我們得到的不是<module>,而是正在被執行的函數的名稱greet()。/<module>

轉到下一個被執行的代碼行,我們看到傳入了一個整數的有問題的greet()調用。

有時在異常引發之後,另一段代碼會捕獲該異常並導致另一個異常。在這些情況下,Python將按照接收異常的順序輸出所有異常回溯,同樣會以最近一次拋出的異常的回溯結束。

這可能有點令人困惑,這裡有一個例子。在greetings.py的底部添加一個對greet_many()的調用。

理解Python Traceback(回溯)

這將會打印出對所有三個人的問候語。但是,如果你運行這段代碼,你會看到一個輸出多個回溯的例子:

理解Python Traceback(回溯)

注意上面輸出中以During handling開始的高亮顯示的行。在所有回溯之間,你將看到這一行。它的消息非常清楚,當你的代碼試圖處理前一個異常時,又引發了另一個異常。

注意: Python的顯示以前異常回溯的特性是在Python 3中添加的。在Python2中,你只會得到最後一個異常的回溯。

你之前已經看到過前面的異常,就在你使用一個整數調用greet()時。因為我們在要打招呼的人員列表中添加了一個1,所以我們可以預期得到相同的結果。但是,函數greet_many()將greet()調用封裝在一個try和except塊中。這樣,當greet()引發異常時,greet_many()會打印一個默認的問候語。

greetings.py的相關部分在這裡被重複:

理解Python Traceback(回溯)


因此,當greet()由於錯誤的整數輸入而導致TypeError時,greet_many()會處理該異常並嘗試打印一個簡單的問候語。這裡的代碼最終會導致另一個類似的異常。它仍然試圖添加一個字符串和一個整數。

查看所有的回溯輸出可以幫助你瞭解異常的真正原因。有時,當你看到最後一個異常被引發,並由此產生回溯時,你仍然看不出哪裡出錯了。在這些情況下,向上移動到前面的異常通常會讓你更好地瞭解根本原因。

Python中有哪些常見的回溯?

在編程時,瞭解如何在程序引發異常時閱讀Python回溯可能非常有用,但是瞭解一些更常見的回溯也可以提升編程的進程。

下面是一些你可能會遇到的常見異常,它們被引發的原因和它們的含義,以及你可以在它們的回溯中找到的信息。

AttributeError

當你試圖訪問一個對象上沒有被定義的屬性時,會引發AttributeError。Python文檔中定義了此異常何時被引發:

當屬性引用或賦值失敗時引發。

下面是一個引發AttributeError的例子:

理解Python Traceback(回溯)

AttributeError的錯誤消息行告訴你,在本例中,具體的對象類型,在本例中是int,沒有訪問的an_attribute屬性。在錯誤消息行中看到AttributeError可以幫助你快速確定你嘗試訪問的是哪個屬性,以及要到哪裡去修復它。

大多數情況下,獲得這個異常表明你正在處理的對象可能不是你期望的類型:

理解Python Traceback(回溯)

在上面的例子中,你可能期望a_list是list類型的,它有一個名為.append()的方法。當你接收到AttributeError異常並看到它是在你嘗試調用.append()時引發的,這說明你正在處理的對象類型可能不是你所期望的。

通常,當你期望從一個函數或方法調用返回一個特定類型的對象時,會出現這種情況,你最終會得到一個類型為None的對象。在本例中,錯誤消息行將寫到,AttributeError: 'None類型'對象沒有屬性'append'。

ImportError

當一個import語句出錯時,ImportError會被引發。如果你試圖導入的模塊找不到,或者你試圖從一個模塊中導入模塊中不存在的內容時,你將得到這個異常,或者它的子類ModuleNotFoundError。Python文檔定義了此異常何時被引發:

當import語句在嘗試加載模塊時遇到困難時引發。當from…import中的“from list”中存在一個無法被找到的名稱時也會引發。

下面是一個ImportError 和ModuleNotFoundError被引發的例子。

理解Python Traceback(回溯)

在上面的例子中,你可以看到,當我們試圖導入不存在的模塊asdf時會導致ModuleNotFoundError。當試圖從一個存在的模塊(這裡是collections)中導入不存在的asdf時,就會導致ImportError。回溯底部的錯誤消息行告訴你,在這兩種情況下都不能導入asdf。

IndexError

當你試圖從一個序列(如列表或元組)中檢索一個索引時,而該索引在這個序列中找不到時,就會引發一個IndexError。Python文檔定義了此異常何時會被引發:

當一個序列的下標超出範圍時引發。

下面是一個引發IndexError的例子:

理解Python Traceback(回溯)

IndexError的錯誤消息行不會給你提供很好的信息。你可以看到有一個超出範圍的序列引用以及此序列的類型,在本例中是一個列表。這些信息,加上其他回溯信息,通常足以幫助你快速確定如何修復此問題。

KeyError

與IndexError類似,當你試圖訪問映射(通常是dict)中沒有的鍵時,會引發KeyError。你可以把它看作是IndexError,只不過是針對字典的。Python文檔定義了此異常何時被引發:

當在現有鍵集合中找不到一個映射(字典)鍵時引發。

下面是一個KeyError被引發的例子:

理解Python Traceback(回溯)

KeyError的錯誤消息行會給出找不到的鍵。這並沒有太多的內容,但是,結合回溯的其他內容,但對於修復這個問題來說通常是足夠了。

要深入瞭解KeyError,請查看《Python KeyError異常以及如何處理它們》。

NameError

當你引用了一個代碼中未定義的變量、模塊、類、函數或其他名稱時,將引發一個NameError。Python文檔定義了此異常何時被引發:

當本地或全局名稱未被找到時引發。

在下面的代碼中,greet()接受一個參數person。但在函數本身中,該參數被錯誤拼寫為persn:

理解Python Traceback(回溯)

NameError 回溯的錯誤消息行給出了缺失的名稱。在上面的例子中,它是一個傳入函數的拼寫錯誤的變量或參數。

如果它是你拼寫錯誤的參數,那麼NameError也會被引發:

理解Python Traceback(回溯)

在這裡,你似乎沒有做錯什麼。在回溯中被執行和引用的最後一行看起來不錯。如果你發現自己處於這種情況,那麼你要做的事情就是查看代碼,確定person變量在哪裡被使用和定義。在這裡,你可以很快看到參數名稱拼錯了。

SyntaxError

當你的代碼中有不正確的Python語法時,就會引發SyntaxError。Python文檔定義了此異常何時被引發:

當解析器產生語法錯誤時引發。

下面代碼的問題是函數定義行末尾缺少一個冒號。在Python REPL中,這個語法錯誤在按下回車鍵後會立即被引發:

理解Python Traceback(回溯)

SyntaxError的錯誤消息行只告訴你代碼的語法有問題。查看上面的行可以得到問題所在的行,通常用a ^(插入符號)指向問題點。這裡,函數的def語句中缺少冒號。

同樣,使用SyntaxError回溯,常規的第一行Traceback (most recent call last:也丟失了。這是因為當Python試圖解析你的代碼時,SyntaxError會被引發,而實際上這些行並沒有被執行。

TypeError

當你的代碼試圖對一個對象執行某些不能執行的操作時,例如試圖將一個字符串相加到一個整數中,或者在一個沒有定義其長度的對象上調用len(),TypeError就會被引發。Python文檔中定義了此異常何時被引發:

當一個操作或函數被應用於一個不合適類型的對象時引發。

下面是TypeError被引發的幾個示例:

理解Python Traceback(回溯)

以上所有引發TypeError的示例都會產生一個包含不同消息的錯誤消息行。每一條消息都能很好地告訴你哪裡出了問題。

前兩個示例嘗試將字符串和整數相加。然而,它們有細微的不同:

  • 第一個試圖將一個str加到一個int。
  • 第二個試圖將一個int 加到一個 str。

錯誤消息行反映了這些不同。

最後一個例子嘗試在一個int上調用len()。錯誤消息行告訴你不能對一個int類型執行此操作。

ValueError

當對象的值不正確時,ValueError將被引發。你可以將其視為一個IndexError,當索引值不在序列範圍之內時會被引發,只不過ValueError用於更一般的情況。Python文檔中定義了此異常何時被引發:

當一個操作或函數接收到一個具有正確類型但值不合適的參數時引發,並且這種情況不能被一個更精確的異常(比如IndexError)描述。

下面是ValueError被引發的兩個例子:

理解Python Traceback(回溯)

在這些例子中,ValueError錯誤消息行會準確地告訴你這些值存在什麼問題:

  1. 在第一個示例中,你試圖解壓縮太多的值。錯誤消息行甚至告訴你,你期望解壓縮3個值,但是隻得到了2個值。
  2. 在第二個例子中,問題是你得到了太多的值,但沒有足夠的變量來解壓縮它們。

如何記錄一個回溯?

獲得異常及其生成的Python回溯意味著你需要決定如何處理它。通常,修復代碼是第一步,但有時問題出在未預期的或不正確的輸入上。雖然在代碼中提供這些情況很好,但有時通過記錄回溯和執行其他操作來隱藏異常也很有意義。

下面是一個更真實的代碼示例,它需要讓一些Python回溯保持靜默。本例使用了requests庫。你可以在Python的requests庫(指南)中獲取更多信息:

理解Python Traceback(回溯)

這段代碼運行得很好。當你運行此腳本時,你將一個URL作為命令行參數提供給它,它將調用該URL,然後打印出HTTP狀態碼和響應中的內容。甚至在響應是一個HTTP錯誤狀態時,它也可以工作:

理解Python Traceback(回溯)

但是,有時你的腳本提供的用於檢索的URL不存在,或者主機服務器關閉。在這些情況下,這個腳本現在就會引發一個未捕獲的ConnectionError異常,並打印一個回溯:

理解Python Traceback(回溯)

這裡的Python回溯可能非常長,還會引發許多其他異常,最終導致ConnectionError被requests庫本身引發。如果你向上移動到最後的異常回溯,你就可以看到問題都是從我們的代碼urlcall .py中的第5行開始的。

如果你將非法行封裝在一個try和except塊中,那麼捕獲適當的異常將允許你的腳本繼續處理更多的輸入:

理解Python Traceback(回溯)

上面的代碼使用了一個帶有try和except塊的else子句。如果你不熟悉Python的這一特性,那麼請在Python Exceptions:An Introduction中查看else子句。

現在,當你使用一個URL來運行此腳本時,將引發一個ConnectionError,系統會打印一個狀態碼-1,以及Connection Error的內容:

理解Python Traceback(回溯)

這運行的很好。然而,在大多數實際系統中,你並不希望只是靜默化異常和生成的回溯,而是希望去記錄回溯。記錄回溯可以讓你更好地理解程序中哪些地方出錯了。

注意: 要了解更多關於Python日誌系統的信息,請查看Python中的logging。

你可以通過導入logging包,獲取一個日誌記錄器並在try和except塊的except部分中調用該日誌記錄器的.exception()來在你的腳本中記錄回溯。你的最終腳本應該會類似於以下代碼:

理解Python Traceback(回溯)

現在,當你對一個有問題的URL運行此腳本時,它會打印預期的-1和Connection Error,同時也會記錄回溯:

理解Python Traceback(回溯)

默認情況下,Python將向標準錯誤(stderr)發送日誌消息。看起來我們根本沒有抑制回溯輸出。但是,如果你在重定向stderr時再次調用它,你可以看到日誌系統正在工作,我們可以將日誌保存起來,以備以後使用:

理解Python Traceback(回溯)

結論

Python回溯包含了大量的信息,可以幫助你發現你的Python代碼中出現的錯誤。這些回溯看起來有點嚇人,但是一旦你把它分解開來,看看它想向你展示什麼,它們就會非常有用。逐行瀏覽一些回溯將會使你更好地理解它們包含的信息,並幫助你最大限度地利用它們。

在運行代碼時獲得Python回溯輸出是改進代碼的一個機會。這是Python試圖幫助你的一種方式。

既然你已經瞭解瞭如何閱讀Python回溯,那麼你可以從學習更多有關診斷回溯輸出所告訴你的問題的一些工具和技術中獲益。Python的內置traceback模塊可用於處理和檢查回溯。當你需要從回溯輸出中獲得更多信息時,traceback模塊是很有用的。瞭解更多有關調試Python代碼的技術也會很有幫助。


英文原文:https://realpython.com/python-traceback/ 譯者:野生大熊貓


分享到:


相關文章: