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 抛出异常