Python 警告,警告過濾器介紹,以及發出和過濾 Python 警告
先決條件
閱讀本節的先決條件是已經了解 Python 例外狀況,你可以檢視Python 例外狀況介紹,以及擲回和擷取 Python 例外狀況一節來獲得相關內容。
Python 警告
Python 中的Warning
(警告)類別及其衍生類別用於表示一些不同於例外狀況的情況,在這些情況下,程式可能並不需要被中斷。比如,你的應用允許使用者使用類似於123456
一樣的密碼,但會通過Warning
類別提示使用者密碼過於簡單。
Python 警告類別可以被當作 Python 例外狀況類別來使用
由於 Python 的Warning
類別繼承自 Python 的Exception
類別,因此,Warning
類別及其衍生類別可以被try…except
陳述式擷取,或被raise
陳述式擲回,只不過,此時的Warning
是一個單純的例外狀況。
在下面的例子中,我們將Warning
類別作為普通的例外狀況來使用,他被raise
陳述式擲回,並被try…except
陳述式擷取。
# 將 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
的回溯資訊。
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
所在的目錄,然後輸入以下命令執行該腳本檔案,你將看到相關的輸出結果。
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)
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)
在下面的範例中,模組student
的warn
函式用於發出警告。由於指定了參數skip_file_prefixes
,並包含了student
模組所在的目錄,因此,在模組school
中呼叫student
的warn
函式之後,警告並不會指向 Python 腳本檔案student.py
,而是會指向父資料夾中的腳本檔案school.py
。
import warnings
import os
def warn():
# 取得 student 模組所在的目錄
filepath = os.path.dirname(__file__)
warnings.warn(
f'警告不會指向符合 {filepath} 的檔案',
skip_file_prefixes=(filepath,)
)
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
會嘗試通過filename
和lineno
參數讀取相關程式碼。
自訂的 showwarning 函式需要自行處理未指定的 file 和 line 參數
Pythonwarnings
模組原有的showwarning
函式,會處理參數file
和line
為空值的情況,如果你定義了自己的showwarning
函式,那麽需要在函式中完成同樣的工作。
在下面的範例中,我們首先呼叫函式showwarning
直接顯示了一條警告,然後定義了函式mywarning
以自訂 Python 警告的顯示方式。
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 警告的文字內容。
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
函式的參數符合某個比對規則,即比對項message
,category
,module
和lineno
時,則該比對規則的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 版本中,預設的比對規則可能會不同。
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
串列的末尾),這表示新加入的規則比原有規則的優先順序更高。除了append
,filterwarnings
函式的參數與之前描述的 Python 警告過濾器比對規則中的各項的含義相同。
filterwarnings(action, message='', category=Warning, module='', lineno=0, append=False)
在下面的範例中,我們使用函式filterwarnings
讓同類的警告只能在同一個 Python 模組中顯示一次,無論他們指向的程式碼位於哪一行。
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 警告過濾器新增比對規則,只不過他缺少了message
與module
兩個參數,這表示simplefilter
函式所新增的比對規則更為簡單。
simplefilter(action, category=Warning, lineno=0, append=False)
在下面的範例中,函式simplefilter
被呼叫了三次,其中第三次新增的規則將替代第二次新增的規則。
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
成為了一個空的串列。
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 參數
參數
action
,category
,lineno
,append
與函式simplefilter
的參數的含義相同,如果action
被指定了一個有效的值,那麽在進入與catch_warnings
物件相關的內容之後,以上四個參數將用於呼叫函式simplefilter
,這相當於在with
陳述式中為 Python 警告過濾器新增了一個比對規則。
在 Python 警告器的預設比對規則中,型別為ImportWarning
的警告一般不會顯示,我們通過with
陳述式和catch_warnings
類別來調整規則,以臨時的顯示所有警告。在with
陳述式結束之後,檢視warnings.filters
表示的比對規則,發現第一項並不是在with
陳述式中新增的規則。
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_warnings
的record
參數設定為True
,with
陳述式中的警告將被儲存至串列log
。
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
,以間接的新增比對規則,這些比對規則應符合下面給出的格式,其各項的含義已經在警告過濾器一段中講述,只不過這裏的message
和module
僅為單純的字串,而非規則運算式,並且其開頭和末尾的空白字元將被忽略。
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
改變這種狀況。
import warnings
# 正常情況下,下面的警告不會顯示
warnings.warn('警告', ImportWarning)
# 正常情況下,下面的警告不會作為例外狀況被擲回
warnings.warn('Err 擲回例外狀況')
python -W default::ImportWarning -W error:Err w.py
…\w.py:3: ImportWarning: 警告
warnings.warn('警告', ImportWarning)
Traceback (most recent call last):
…
UserWarning: Err 擲回例外狀況
python3 -W default::ImportWarning -W error:Err w.py
…/w.py:3: ImportWarning: 警告
warnings.warn('警告', ImportWarning)
Traceback (most recent call last):
…
UserWarning: Err 擲回例外狀況