Python 警告,警告過濾器介紹,以及發出和過濾 Python 警告

閱讀 29:48·字數 8944·發佈 
Youtube 頻道
訂閱 133

先決條件

閱讀本節的先決條件是已經了解 Python 例外狀況,你可以檢視Python 例外狀況介紹,以及擲回和擷取 Python 例外狀況一節來獲得相關內容。

Python 警告

Python 中的Warning(警告)類別及其衍生類別用於表示一些不同於例外狀況的情況,在這些情況下,程式可能並不需要被中斷。比如,你的應用允許使用者使用類似於123456一樣的密碼,但會通過Warning類別提示使用者密碼過於簡單。

Python 警告類別可以被當作 Python 例外狀況類別來使用

由於 Python 的Warning類別繼承自 Python 的Exception類別,因此,Warning類別及其衍生類別可以被try…except陳述式擷取,或被raise陳述式擲回,只不過,此時的Warning是一個單純的例外狀況。

在下面的例子中,我們將Warning類別作為普通的例外狀況來使用,他被raise陳述式擲回,並被try…except陳述式擷取。

raise.py
# 將 Warning 類別作為例外狀況來使用
try:
	raise Warning
except Warning as err:
	print(type(err))
<class 'Warning'>

發出 Python 警告

在 Python 中,應使用warnings模組的warn函式來建立並發出一個警告,而不是通過raise陳述式。Python 警告並不像例外狀況一樣,需要try…except陳述式的處理,發出 Python 警告通常不會導致程式的中斷。

warn(message, category=None, stacklevel=1, source=None, *, skip_file_prefixes=None)

message 參數

message參數是一個字串(將作為警告資訊的一部分),或者一個 Python 警告物件(Warning類別或其衍生類別的執行個體)。如果是一個 Python 警告物件,那麽運算式str(message)的傳回值將作為警告資訊的一部分。

category 參數

category參數是 Python 類別Warning或者該類別的衍生類別(預設為類別UserWarning),用於表示warn函式所建立的 Python 警告的型別。當message參數被指定為一個警告物件時,category參數將被忽略,此時,警告的型別被視為與message.__class__相同。

stacklevel 參數

stacklevel參數是堆疊的層級,預設值為1,這表示warn函式產生的 Python 警告將指向呼叫warn函式的程式碼。如果你在某個函式中呼叫了warn,那麽可以將stacklevel設定為2,Python 警告將指向呼叫該函式的程式碼,而不是呼叫warn的程式碼。

source 參數

source參數應該是一個有效的 Python 物件(可充當導致警告的原因),他的回溯資訊將顯示在警告中,要實作這種效果,你可能需要在命令列中使用參數-X tracemalloc來執行相關的 Python 腳本,否則將會收到提示Warning: Enable tracemalloc to get the object allocation traceback

skip_file_prefixes 參數

skip_file_prefixes參數是一個表示檔案路徑的字串元組,如果 Python 腳本檔案的路徑的開始部分,與元組中的某一元素相符(區分大小寫),那麽warn函式產生的警告不會指向該檔案所包含的程式碼,這意味著警告可能被定位至其他腳本檔案。如果skip_file_prefixes參數被有效的設定,那麽stacklevel參數將被修改為運算式max(2,stacklevel)的計算結果,也就是說,警告不會指向直接呼叫warn函式的程式碼。

warn 函式的 skip_file_prefixes 參數不應包含 Python 腳本檔案的副檔名部分

如果你希望在warn函式的skip_file_prefixes參數中包含 Python 腳本檔案的路徑,那麽需要去掉檔案的副檔名部分(比如,.py),否則可能不會產生任何效果。

在下面的範例中,函式check用於檢查日期並呼叫warn函式發出警告。參數stacklevel被設定為2,因此,警告將指向 Python 腳本檔案的第 12 行,而不是呼叫warn函式的第 9 行,參數source被設定為today,因此,將顯示today的回溯資訊。

issue.py
import warnings
from datetime import date

# 檢查日期的函式 def check(today): # 如果是 11 月 11 日,則發出警告 if today.month == 11 and today.day == 11: # 警告將指向 check 的呼叫者,並顯示 today 的回溯資訊 warnings.warn('11 月 11 日?', Warning, 2, today)
goodday = date(2024, 11, 11) check(goodday)

將命令列跳躍至 Python 腳本檔案issue.py所在的目錄,然後輸入以下命令執行該腳本檔案,你將看到相關的輸出結果。

Windows
python -X tracemalloc issue.py
\issue.py:12: Warning: 11 月 11 日?
  check(goodday)
Object allocated at (most recent call last):
  File "\issue.py", lineno 11
    goodday = date(2024, 11, 11)
UNIX/Linux/macOS
python3 -X tracemalloc issue.py
/issue.py:12: Warning: 11 月 11 日?
  check(goodday)
Object allocated at (most recent call last):
  File "/issue.py", lineno 11
    goodday = date(2024, 11, 11)

在下面的範例中,模組studentwarn函式用於發出警告。由於指定了參數skip_file_prefixes,並包含了student模組所在的目錄,因此,在模組school中呼叫studentwarn函式之後,警告並不會指向 Python 腳本檔案student.py,而是會指向父資料夾中的腳本檔案school.py

skip/student.py
import warnings
import os

def warn(): # 取得 student 模組所在的目錄 filepath = os.path.dirname(__file__) warnings.warn( f'警告不會指向符合 {filepath} 的檔案', skip_file_prefixes=(filepath,) )
school.py
from skip import student
# 呼叫 student 模組的 warn 函式
student.warn()
# Windows 中的輸出結果
\school.py:3: UserWarning: 警告不會指向符合 \skip 的檔案
  student.warn()

自訂 Python 警告的顯示方式

在一般情況下,通過 Pythonwarnings模組的warn函式發出的警告,其相關資訊將被傳遞至sys模組的stderr變數,他是一個用於處理 Python 錯誤資訊的標準檔案物件,該物件在接收到警告的相關資訊之後,會輸出這些資訊,比如,顯示在命令列中。以上這些工作,由warnings模組的showwarning函式完成,通過編寫一個新的函式或方法來代替他(將新的函式或方法指派給warnings.showwarning),即可自訂 Python 警告資訊的顯示方式。

當然,你也可以直接呼叫 Pythonwarnings模組的showwarning函式來顯示警告資訊,但這種情況很少見。

showwarning(message, category, filename, lineno, file=None, line=None)

message 參數

message參數是一個字串(將作為警告資訊的一部分),或者一個 Python 警告物件(Warning類別或其衍生類別的執行個體)。

category 參數

category參數是 Python 警告的型別(Warning類別或其衍生類別)。

filename 參數

filename參數表示與 Python 警告相關聯的腳本檔案的路徑。

lineno 參數

lineno參數表示與 Python 警告相關聯的程式碼的行號。

file 參數

file參數是用於輸出 Python 警告資訊的物件,如果未指定,則showwarning會嘗試使用sys.stderr

line 參數

line參數是與 Python 警告相關聯的程式碼,如果未指定,則showwarning會嘗試通過filenamelineno參數讀取相關程式碼。

自訂的 showwarning 函式需要自行處理未指定的 file 和 line 參數

Pythonwarnings模組原有的showwarning函式,會處理參數fileline為空值的情況,如果你定義了自己的showwarning函式,那麽需要在函式中完成同樣的工作。

在下面的範例中,我們首先呼叫函式showwarning直接顯示了一條警告,然後定義了函式mywarning以自訂 Python 警告的顯示方式。

showwarning.py
import warnings

# 第 4 行程式碼將作為警告資訊的一部分 message = '直接呼叫 showwarning' warnings.showwarning( message, UserWarning, filename=__file__, lineno=4 )
# 用於顯示警告的函式 mywarning def mywarning(message, category, filename, lineno, file=None, line=None):
# 處理 file,line 參數為 None 的情況 if not file: import sys file = sys.stderr
if not line: line = open(filename, encoding='utf8').readlines()[lineno - 1]

file.writelines(f'自訂警告:{message} {category} {line}')
# 用 mywarning 取代原有的 showwarning 函式 warnings.showwarning = mywarning warnings.warn('再次警告')
# Windows 中的輸出結果
\test.py:4: UserWarning: 直接呼叫 showwarning
  message = '直接呼叫 showwarning'
自訂警告:再次警告 <class 'UserWarning'> warnings.warn('再次警告')

如果你並不想修改整個 Python 警告的顯示方式,僅希望自訂 Python 警告資訊中的文字內容,那麽可以編寫一個新的函式或方法來替代warnings模組原有的formatwarning函式。formatwarning函式與showwarning的參數的含義一致,但由於沒有file參數,自訂的formatwarning函式僅需要處理line參數為空值的情況。

formatwarning(message, category, filename, lineno, line=None)

formatwarning 函式由 Python warnings 模組原有的 showwarning 函式呼叫

當然,直接呼叫formatwarning函式將得到包含 Python 警告資訊的字串,但這種情況很少見,因為formatwarning函式總是由warnings模組原有的showwarning函式來呼叫,原有的showwarning函式會將formatwarning函式的傳回值寫入到file參數表示的 Python 檔案物件中。

在下面的範例中,我們使用函式myinfo來自訂 Python 警告的文字內容。

formatwarning.py
import warnings

# 用於格式化警告資訊的函式 myinfo def myinfo(message, category, filename, lineno, line=None):
# 處理 line 參數為 None 的情況 if not line: line = open(filename, encoding='utf8').readlines()[lineno - 1]
return f'簡化的資訊:{line} {category} {message}'
# 用 myinfo 取代原有的 formatwarning 函式 warnings.formatwarning = myinfo warnings.warn('一個警告')
簡化的資訊:warnings.warn('一個警告')
 <class 'UserWarning'> 一個警告

Python 警告過濾器

事實上,Pythonwarnings模組的warn函式會根據參數來決定下一步的動作,警告可能會被轉換為一個 Python 例外狀況,也可能會被忽略。所有的這一切均由 Python 警告過濾器來控製,該過濾器是一個 Python 串列,對應了warnings模組的filters變數,串列中包含了一系列的比對規則,每一個規則均對應一個 Python 元組,其格式為(action,message,category,module,lineno)(元組中的每一項的作用將在下面列出)。當warn函式的參數符合某個比對規則,即比對項messagecategorymodulelineno時,則該比對規則的action項,將指示warn函式如何進行下一步的操作。

action 項

action是一個字串,用來指示warn函式如何處理目前的 Python 警告,他擁有以下的有效取值,'default'表示指向相同模組的相同程式碼的所有同類警告,僅第一個會被顯示,'error'表示會將警告作為 Python 例外狀況擲回,'ignore'表示不會顯示警告資訊(即忽略警告),'always'表示總是顯示警告的資訊,'module'表示指向相同模組的所有同類警告,僅第一個會被顯示,'once'表示所有相符的同類警告,僅第一個會被顯示(實際上,'once''module'所實作的效果幾乎沒有差別)。

message 項

message項是一個表示規則運算式的字串,用於比對warn函式的message參數,message參數對應的字串的開頭部分應該與message項相符(不區分大小寫)。如果未指定message項,那麽message參數將不需要進行比對。

category 項

category項是Warning類別或其衍生類別,這表示警告的型別應該是category表示的類別或其衍生類別。

module 項

module項是一個表示規則運算式的字串,用於比對警告對應的模組,模組的完整名稱的開頭部分應該與module項相符(區分大小寫)。如果未指定module項,則表示與任意模組均相符。

lineno 項

lineno項是一個表示行號的整數,警告對應的行號應該與lineno項相符。如果lineno項為0,則表示與任意行號均相符。

當 warn 函式的參數未能與 Python 警告過濾器的任何規則相符時

如果warnings模組的warn函式的參數,未能與 Python 警告過濾器的任何規則相符,那麽warn函式接下來進行的操作,會等同於將action項設定為'default'的情況。

Python 警告過濾器的比對規則可以與不同類的警告相符

你可以將message參數和警告型別相同的 Python 警告視為同類警告,警告過濾器的同一個比對規則能夠與不同類的警告相符。

Python 警告過濾器的預設比對規則

除了偵錯版本,Python 的警告過濾器會包含一些預設的比對規則,你可以通過warnings模組的filters變數來檢視他們。當然,在不同的 Python 版本中,預設的比對規則可能會不同。

default_rules.py
import warnings

# 顯示所有的預設比對規則 for rule in warnings.filters: print(rule)
('default', None, <class 'DeprecationWarning'>, '__main__', 0)
('ignore', None, <class 'DeprecationWarning'>, None, 0)
('ignore', None, <class 'PendingDeprecationWarning'>, None, 0)
('ignore', None, <class 'ImportWarning'>, None, 0)
('ignore', None, <class 'ResourceWarning'>, None, 0)

為 Python 警告過濾器新增比對規則

在某些情況下,你可能想為 Python 警告過濾器新增比對規則,以修改一些警告的處理方法,warnings模組的filterwarnings函式可以完成此目標,通過該函式新增的比對規則,將成為warnings模組的filters串列的第一個元素(可將append參數設定為True,以讓新規則被新增至filters串列的末尾),這表示新加入的規則比原有規則的優先順序更高。除了appendfilterwarnings函式的參數與之前描述的 Python 警告過濾器比對規則中的各項的含義相同。

filterwarnings(action, message='', category=Warning, module='', lineno=0, append=False)

在下面的範例中,我們使用函式filterwarnings讓同類的警告只能在同一個 Python 模組中顯示一次,無論他們指向的程式碼位於哪一行。

same_module.py
import warnings

# 指向同一模組的同類警告,僅顯示一次 warnings.filterwarnings('module')
# 顯示警告 A def test(): warnings.warn('警告 A', UserWarning)
# test 函式中的警告將被顯示 test() # 下面的警告不會被顯示,因為他與 test 中的警告屬於同一類 warnings.warn('警告 A', UserWarning)

# 顯示警告 B # 警告 B 將被顯示,因為他與警告 A 不是同一類 warnings.warn('警告 B', UserWarning) from datetime import date today = date(2024, 11, 11) # 下面的警告不會被顯示,因為他與上一個警告 B 屬於同一類 warnings.warn('警告 B', UserWarning, source=today) # 下面的警告會被顯示,因為他指向了另一個模組 warnings.warn('警告 B', UserWarning, stacklevel=2) # 下面的警告不會被顯示,因為他與上一個警告 B 屬於同一類,並且指向了同一個模組 import os warnings.warn('警告 B', UserWarning, skip_file_prefixes=(os.path.dirname(__file__),))
# Windows 中的輸出結果
\same_module.py:7: UserWarning: 警告 A
  warnings.warn('警告 A', UserWarning)
\same_module.py:15: UserWarning: 警告 B
  warnings.warn('警告 B', UserWarning)
sys:1: UserWarning: 警告 B

除了filterwarnings函式,warnings模組的simplefilter函式同樣可以用於為 Python 警告過濾器新增比對規則,只不過他缺少了messagemodule兩個參數,這表示simplefilter函式所新增的比對規則更為簡單。

simplefilter(action, category=Warning, lineno=0, append=False)

在下面的範例中,函式simplefilter被呼叫了三次,其中第三次新增的規則將替代第二次新增的規則。

simple.py
import warnings

# 忽略行號為 5 的型別為 UserWarning 的警告 warnings.simplefilter('ignore', UserWarning, 5) warnings.warn('我被忽略了', UserWarning)
# 忽略行號為 11 的警告 warnings.simplefilter('ignore', lineno=11) # 下面的規則將替代上面的規則生效 warnings.simplefilter('always', lineno=11) warnings.warn('顯示')
# Windows 中的輸出結果
\simple.py:11: UserWarning: 顯示
  warnings.warn('顯示')

清空 Python 警告過濾器的所有比對規則

如果你已經為 Python 警告器新增了足夠多的比對規則,現在想要刪除他們,那麽可以使用warnings模組的resetwarnings函式,該函式會清空 Python 警告器中的所有比對規則,包括預設的比對規則在內,這會讓warnings模組的filters變數成為一個空的 Python 串列。

resetwarnings()

在下面的範例中,我們使用resetwarnings清空了所有的比對規則,通過print函式可以發現,warnings.filters成為了一個空的串列。

reset.py
import warnings

# 為警告過濾器新增比對規則 # 忽略 message 參數開頭符合規則運算式 Err_.+ 的警告 warnings.filterwarnings('ignore', 'Err_.+') # 下面的警告不會顯示,因為 message 的開頭符合 Err_.+(不區分大小寫) warnings.warn('eRR_A 糟糕!我又吃癟了?') # 下面的警告會顯示,因為 message 的開頭含有一個空格,與 Err_.+ 不相符 warnings.warn(' Err_A 拒絕吃癟')
# 清空 Python 警告過濾器的規則 warnings.resetwarnings() print(warnings.filters)
# 下面的警告會顯示,因為清空了所有規則 warnings.warn('Err_A 糟糕!我又吃癟了?')
# Windows 中的輸出結果
\reset.py:9: UserWarning:  Err_A 拒絕吃癟
  warnings.warn(' Err_A 拒絕吃癟')
[]
\reset.py:16: UserWarning: Err_A 糟糕!我又吃癟了?
  warnings.warn('Err_A 糟糕!我又吃癟了?')

臨時改變 Python 警告過濾器的比對規則

無論是為 Python 警告器新增比對規則,還是通過resetwarnings清空比對規則,你都可能希望這是一種臨時的改變,尤其是通過新增比對規則來測試一些警告的時候。使用warnings模組的catch_warnings類別和with陳述式可以完成上述目標,在通過with陳述式進入與catch_warnings物件相關的內容之後,對 Python 警告過濾器的比對規則以及showwarning函式的修改將是臨時性的。

catch_warnings(*, record=False, module=None, action=None, category=Warning, lineno=0, append=False)

record 參數

record參數用於指示在進入與catch_warnings物件相關的內容之後,是否將發出的警告記錄到一個串列中(每個警告將對應一個包含警告相關內容的WarningMessage物件),預設值為False,不記錄。如果record參數被設定為True,那麽可通過with陳述式的as關鍵字將儲存警告的串列繫結至某個 Python 變數,這樣做的好處在於,with陳述式中發出的警告不會直接顯示,你可以更容易的得知catch_warnings所產生的效果。如果record參數被設定為False,那麽儲存警告的串列並不會存在。

module 參數

module參數用於表示進入與catch_warnings物件相關的內容之前的warnings模組,catch_warnings物件將備份該模組的filters(即 Python 警告過濾器的比對規則)和showwarning等特性,以便在with陳述式結束時恢復他們。如果未指定module參數,那麽將使用sys.modules中的warnings模組。一般情況下,module參數僅用於測試warnings模組本身,你可以將自己改寫的warnings模組指派給該參數,以檢視其執行效果。

action,category,lineno,append 參數

參數actioncategorylinenoappend與函式simplefilter的參數的含義相同,如果action被指定了一個有效的值,那麽在進入與catch_warnings物件相關的內容之後,以上四個參數將用於呼叫函式simplefilter,這相當於在with陳述式中為 Python 警告過濾器新增了一個比對規則。

在 Python 警告器的預設比對規則中,型別為ImportWarning的警告一般不會顯示,我們通過with陳述式和catch_warnings類別來調整規則,以臨時的顯示所有警告。在with陳述式結束之後,檢視warnings.filters表示的比對規則,發現第一項並不是在with陳述式中新增的規則。

show_all.py
import warnings

# ImportWarning 在預設規則中不會顯示 warnings.warn('預設不會顯示的警告', ImportWarning)
# 臨時讓所有警告均被顯示 with warnings.catch_warnings(action='always'): warnings.warn('現在可以顯示 ImportWarning', ImportWarning)
# 檢視比對規則的第一條 print(warnings.filters[0])
# Windows 中的輸出結果
\show_all.py:8: ImportWarning: 現在可以顯示 ImportWarning
  warnings.warn('現在可以顯示 ImportWarning', ImportWarning)
('default', None, <class 'DeprecationWarning'>, '__main__', 0)

在下面的程式碼中,我們把catch_warningsrecord參數設定為Truewith陳述式中的警告將被儲存至串列log

record.py
import warnings

# 將警告儲存在 log 串列中 with warnings.catch_warnings(record=True) as log: warnings.warn('第一個警告', UserWarning) # 下面的警告不會被儲存至 log,因為該警告本身會被忽略 warnings.warn('第二個警告', ImportWarning)
# 顯示 with 陳述式中發出的警告 for warning in log: print(f'{warning.message} {warning.lineno}')
第一個警告 5

通過命令列參數 -W 或環境變數 PYTHONWARNINGS 為 Python 警告過濾器新增比對規則

在某些情況下,你可能無法或不想通過編寫程式碼來調整警告過濾器的比對規則,Python 提供了命令列參數-W和環境變數PYTHONWARNINGS,以間接的新增比對規則,這些比對規則應符合下面給出的格式,其各項的含義已經在警告過濾器一段中講述,只不過這裏的messagemodule僅為單純的字串,而非規則運算式,並且其開頭和末尾的空白字元將被忽略。

action:message:category:module:lineno

如果希望省略某個項,只需要將其留空即可。如果希望新增多個比對規則,則需要在環境變數PYTHONWARNINGS中使用,分隔他們(比如,ignore,error::UserWarning),或者書寫多個-W參數(比如,-W ignore -W error::UserWarning)。

除了命令列參數-W,你還可以使用-Wd-Wdefault(等同於-W default),-We-Werror(等同於-W error),-Wa-Walways(等同於-W always),-Wm-Wmodule(等同於-W module),-Wo-Wonce(等同於-W once),-Wi-Wignore(等同於-W ignore)等選項。

設定環境變數 PYTHONWARNINGS 後可能需要重新啟動才能生效

如果希望通過環境變數PYTHONWARNINGS來新增比對規則,那麽在對其設定後,你可能需要重新啟動一些應用程式,以使對PYTHONWARNINGS的改動生效。

如何通過 Python 警告檢查程式碼是否使用了已經取代的內容?

如果未作出任何調整,那麽關於取代內容的警告DeprecationWarning可能不會顯示,這是可以理解的,因為在程式被正常使用時,這些警告資訊不應該展示給使用者。不過,你可以通過新增命令列選項-Wd-Wdefault-W default::DeprecationWarning,來確認程式碼是否使用了已經取代的內容。

當然,要完成以上目標,設定環境變數PYTHONWARNINGS或直接通過warnings模組修改比對規則也是可行的。

設定環境變數

關於如何設定環境變數,你可以檢視程式設計指南如何設定 Windows 環境變數如何設定 UNIX/Linux/macOS 環境變數兩節。

在預設情況下,Python 腳本檔案w.py中的第一個警告不會顯示,第二個警告不會被轉換為例外狀況,但我們通過參數-W改變這種狀況。

w.py
import warnings
# 正常情況下,下面的警告不會顯示
warnings.warn('警告', ImportWarning)
# 正常情況下,下面的警告不會作為例外狀況被擲回
warnings.warn('Err 擲回例外狀況')
Windows
python -W default::ImportWarning -W error:Err w.py
\w.py:3: ImportWarning: 警告
  warnings.warn('警告', ImportWarning)
Traceback (most recent call last):

UserWarning: Err 擲回例外狀況
UNIX/Linux/macOS
python3 -W default::ImportWarning -W error:Err w.py
/w.py:3: ImportWarning: 警告
  warnings.warn('警告', ImportWarning)
Traceback (most recent call last):

UserWarning: Err 擲回例外狀況

程式碼

src/zh-hant/exceptions/warnings·codebeatme/python·GitHub