在Python中處理警告

在Python中處理警告

我們每個人都會遇到這種情況: 你寫了一些Python代碼,但是你遇到了一個錯誤:

在Python中處理警告

這不僅僅是一個錯誤,而是一個異常。這是Python以明確的方式表述存在問題的方式,這樣,我們就可以用“try”和“except”關鍵字來捕獲它。

就像Python中的其他東西一樣,異常也是一個對象。這意味著一個異常有一個類——我們就是用這個類來捕獲異常的:

在Python中處理警告

我們甚至可以有幾個“except”子句,每個子句會尋找一種不同類型的錯誤。但是,每個Python類(除了“object”)都繼承自其他類,對於異常類也是如此。因此,如果我們想同時捕獲“KeyError”和“IndexError”,那麼我們可以顯式地命名它們。或者我們可以只捕獲“LookupError”,它是“KeyError”和“IndexError”的父類。

在Python標準庫文檔中,你可以在https://docs.python.org/3/library/exceptions.html 中查看Python的異常類層次結構。它有助於你瞭解Python中存在哪些異常,層次結構如何,以及一般地理解異常如何工作。

但是,如果你查看該層次結構的底部,你將看到一個名為“Warning”的異常類,以及一些子類,比如“DeprecationWarning”和“BytesWarning”。這些是什麼?雖然它們與異常層次結構一起被包含在其中,警告也是異常,但它們既不會像正常的異常那樣被拋出,也不會像正常的異常那樣被使用。它們是什麼,我們要如何使用它們?

首先,回顧一下歷史: Warnings在Python中已經存在很長時間了,從Python 2.1開始(追溯到2000年)。PEP 230是由Guido van Rossum (Python的創造者和長期的“仁慈的生活的獨裁者”)編寫的,它的添加不僅是為了創建一種機制來提醒用戶可能出現的問題,而且是為了從程序內部發送此類警告並決定如何處理它們。

為什麼要用警告?

在我向你展示如何在你自己的代碼中使用警告之前,讓我們首先考慮一下為什麼以及何時需要使用警告。畢竟,你總是可以使用“print”來顯示警告。或者,如果確實有問題,那麼你也可以拋出一個異常。

但這正是問題的關鍵:在某些時候,你想要吸引用戶的注意,但又不需要停止程序或強制執行try-except子句。雖然“print”總是很有用,但它通常會寫入標準輸出(也就是Python中的“sys.stdout”),這意味著你的警告可能會與系統的警告本身混合在一起。

(雖然我剛寫過,你可能會想要引起用戶的注意,但我認為,大多數情況下,警告是針對開發人員而不是用戶的。Python中的警告有點像汽車上的“所需的服務”燈;用戶可能知道有些地方出了問題,但是隻有一個經過認證的維修人員才會知道該怎麼做。開發人員應該避免向最終用戶顯示警告。)

你還可以想象這樣一種情況,在這種情況下,某些警告比其他警告更重要。你當然可以設計一種方案,在該方案中,程序會“print”警告,並且該警告的第一個字符將指示警告的嚴重程度……但是,在Python有一個完整的對象系統,以及可以自由使用的複雜數據類型的情況下,我們為什麼要這樣做呢?

此外,在某些情況下,用戶可能不希望忽略警告。也許我在生產環境中運行的是一種非常敏感的程序,我寧願讓程序提前退出,也不願在一個潛在的不確定情況下繼續運行。

Python的警告系統考慮到了這一切:

  • 它將警告看作一個單獨的輸出類型 , 這樣我們就不會將它與異常或者程序的打印文本相混淆。

  • 它允許我們指明我們正在發送給用戶哪種警告,

  • 它可以讓用戶指示如何處理不同類型的警告,讓一些引發嚴重錯誤,其他的在屏幕上顯示它們的信息,還有一些始終被忽略,

  • 它可以讓程序員開發它們自己的、新的警告類型。

並不是每個程序都需要有或使用警告。但是你的程序可能會因為加載模塊或調用函數的方式而責罵用戶,因此,Python的警告系統就為你提供了你想要的功能。

警告用戶

假設你想要警告用戶某些事情。你可以通過導入“warnings”模塊來實現這一點,然後使用“warnings.warn”來告訴他們出現了什麼問題:

在Python中處理警告

當我運行上面的代碼(在一個名為“warnings1.py”的文件中)時,會發生什麼呢?輸出如下:

在Python中處理警告

換句話說,上面的內容都被寫到了我的終端屏幕上。這三行代碼都是按順序打印的,所以警告並不是在程序的單獨階段(例如,編譯)被打印的。但是在“print”語句的文本和我從警告中得到的輸出之間有一個明顯的區別。

首先,我們被告知警告發生在哪個文件中,在哪一行。在這樣一個小而瑣碎的例子中,這似乎有些過分。但是,如果你有一個包含許多不同文件的大型應用程序,那麼知道是什麼代碼生成了警告無疑是很好的。

我們還被告知這是一個“UserWarning”—我們可以生成的警告類型之一。正如不同類型的異常允許我們選擇性地捕獲它們一樣,不同類型的警告也允許我們以不同的方式處理它們。

但是在這個輸出中還有一些隱藏的東西:“print”語句和我的“warnings.warn”語句實際上將它們的輸出發送到了兩個不同的地方。正如我上面寫的,“print”通常會寫到“標準輸出”,也就是“sys.stdout”,它通常會連接到用戶的終端窗口。但“warnings.warn”通常會寫到“標準錯誤”,也就是“sys.stderr”。問題是,在默認情況下,“sys.stdout” 和 “sys.stderr”都會寫到同一個地方,即用戶的終端。

但是如果我把程序輸出重定向到一個文件中,我們來看看會發生什麼:

在Python中處理警告

我告訴我的Unix shell我想運行“warnings1.py”,所有的輸出都應該被放在“output.txt”中,而不是顯示在屏幕上。但我並沒有說“全部輸出”。相反,通過使用“>”,我只重定向了發送到“sys.stdout”的輸出。被髮送到“sys.stderr”的警告仍然被顯示出來了。這通常被認為是一件好事,它可以確保即使你將輸出重定向到一個文件,你仍然能夠看到警告和其他錯誤。因此,儘管sys.stdout 和 sys.stderr 在默認情況下都會去同一個目的地,但我們也可以看到將它們分開的好處。

不同類型的警告

假設我正在維護一個已經存在了一段時間的庫。這個庫有一個有用的函數,但是這個函數有點過時了,並且不支持現代的用例。作為該庫的維護者,支持該函數的兩個版本(舊版本和新版本)對我來說是一件痛苦的事情。

我可以在文檔和社交媒體中聲明,我的庫的新版本(3.0)將於明年釋出,而且這個新版本將不再支持該函數的舊版本。但是我們都知道程序員並不傾向於閱讀文檔。因此,我更願意讓用戶感到震驚,告訴他們雖然舊的函數版本仍然可以運行,但他們應該開始轉向更新的版本。

我該怎麼做呢?當然是使用警告!下面是一個例子:

在Python中處理警告

現在,只要用戶運行了“hello”函數,他們就會得到一個警告。此外,因為這個警告會寫到標準錯誤(而不是標準輸出),所以它不會與正常輸出混在一起。下面是來自上面代碼的輸出:

在Python中處理警告

但還有比這更好的方法:或許我們想把普通的、乏味的警告與其他類型的警告區分開來。例如,我們可能有許多被廢棄的函數。為了處理這中情況,“warnings.warn”函數支持可選的第二個參數——警告的類別。例如,我們可以使用DeprecationWarning:

在Python中處理警告

我們不必“import”DeprecationWarning或任何其他的標準警告類型,因為它們已經被自動導入到了Python程序始終可用的“內置”命名空間中。這樣一來,就有許多這樣的警告類可以供我們使用,包括UserWarning(默認)、DeprecationWarning(我們在這裡使用了)、SyntaxWarning和UnicodeWarning。你可以使用其中你認為最合適的一個。

你可能已經注意到,這些警告類別與我們在前面查看Python的內置異常層次結構時所看到的類完全相同。實際上,這些類就是這樣被使用的,作為第二個參數傳遞給“warnings.warn”。

簡單的過濾

假設你正在使用一堆舊函數,每一個函數都會提醒你,你應該切換到它們的新的替代版本。如果每次運行程序時都收到一堆警告,你可能會有點惱火。警告是用來通知你你應該進行升級……但是有時候,這些警告與其說有用,不如說是煩人。

在這種情況下,你可能希望過濾掉一些警告。現在,“filtering”是警告系統使用的一個非常普遍的術語。它基本上是讓你說,“當一個匹配特定條件的警告觸發時,使用它做X”——X可以是各種各樣的事情。

最簡單的過濾器是“warnings.simplefilter”,而調用它的最簡單方法是使用單個字符串參數。這個參數告訴警告系統,如果它遇到一個警告,該怎麼做:

  • “默認”——在警告第一次出現時顯示它

  • “錯誤”——將警告轉換成一個異常

  • “忽略”——忽略警告

  • “總是”——總是顯示警告,即使它以前被顯示過

  • “模塊”——每個模塊顯示一次警告

  • “一次”——在整個程序中只顯示一次警告

例如,如果我想忽略所有警告,我可以這樣寫:

在Python中處理警告

並且如果我有這樣的代碼:

在Python中處理警告

我們會看到這樣的輸出:

在Python中處理警告

如你所見,由於使用了“ignore”,警告完全消失了。

如果我們採取另一種極端,即將警告轉換為異常,會發生什麼?

在Python中處理警告

果然,我們得到了一個異常:

在Python中處理警告

正如你所看到的,我們得到了一個UserWarning異常。我們可以在這些警告上面使用“try”和“except”,如果我們想的話,我們就可以捕獲它們……儘管我必須承認,在我看來,將警告轉換成異常只是為了捕獲它們是很奇怪的。(不過,我相信有這樣的用例。)

更具體的過濾

我提到過“simplefilter”採用了一個強制性的參數,並且我們已經看到了這些參數可以是什麼。但事實證明,“simplefilter”使用了幾個額外的、可選的參數,這些參數可用於指定一個警告被髮出時將發生什麼。

例如,假設我想忽略UserWarning,並將DeprecationWarning轉換為異常。我可以這樣寫:

在Python中處理警告

這段代碼會生成以下輸出:

在Python中處理警告

換句話說,我們成功地忽略了一種類型的警告,同時將另一種類型的警告轉換為異常——與所有異常一樣,如果忽略這種異常,它將是致命的。

“simplefilter”函數接受四個參數,除了第一個參數外,其他參數都是可選的

你還能做什麼?

警告系統可以處理非常廣泛的各種情況,並且可以通過多種方式進行配置。除此之外,你還可以:

  • 使用-W標誌從命令行定義警告過濾器

  • 設置多個過濾器,每個過濾器處理一種不同的情況

  • 指定應該過濾的消息和模塊,可以以一個字符串,也可以以一個正則表達式

  • 創建你自己的警告,作為現有警告類的子類

  • 使用Python的日誌模塊捕獲警告,而不是將輸出打印到sys.stderr。

  • 將輸出傳遞到一個你選擇的可調用對象(即函數或類),而不是sys.stderr,用於更高級的處理。

  • 關於警告的原始文檔(PEP 230)是一個很好的起點,它還描述了引入警告的動機。

  • 標準庫中的“warnings”模塊文檔描述了我在這裡所寫的所有內容,以及更多的內容。

  • “本週Python模塊”網站有一個很好的介紹:https://pymotw.com/3/warnings/


英文原文:https://lerner.co.il/2020/04/27/working-with-warnings-in-python/
譯者:天天向上在Python中處理警告


分享到:


相關文章: