Python 警告,警告过滤器介绍,以及发出和过滤警告
关注 1800
前提
阅读本节的前提是已经了解 Python 异常,你可以查看Python 异常介绍,以及抛出和捕获异常一节来获得相关内容。
Python 警告
Python 中的Warning(警告)类及其派生类用于表示一些不同于异常的情况,在这些情况下,程序可能并不需要被中断。比如,你的应用允许用户使用类似于123456一样的密码,但会通过Warning类提示用户密码过于简单。
警告类可以被当作异常类来使用
由于Warning类继承自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语句的处理,发出警告通常不会导致程序的中断。
warn(message, category=None, stacklevel=1, source=None, *, skip_file_prefixes=None)
- message 参数
message参数是一个字符串(将作为警告信息的一部分),或者一个警告对象(Warning类或其派生类的实例)。如果是一个警告对象,那么表达式str(message)的返回值将作为警告信息的一部分。- category 参数
category参数是类Warning或者该类的派生类(默认为类UserWarning),用于表示warn函数所创建的警告的类型。当message参数被指定为一个警告对象时,category参数将被忽略,此时,警告的类型被视为与message.__class__相同。- stacklevel 参数
stacklevel参数是栈的级别,默认值为1,这表示warn函数产生的警告将指向调用warn函数的代码。如果你在某个函数中调用了warn,那么可以将stacklevel设置为2,警告将指向调用该函数的代码,而不是调用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函数的代码。
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 警告的显示方式
在一般情况下,通过warnings模块的warn函数发出的警告,其相关信息将被传递至sys模块的stderr变量,他是一个用于处理错误信息的标准文件对象,该对象在接收到警告的相关信息之后,会输出这些信息,比如,显示在命令行中。以上这些工作,由warnings模块的showwarning函数完成,通过编写一个新的函数或方法来代替他(将新的函数或方法赋值给warnings.showwarning),即可自定义警告信息的显示方式。
当然,你也可以直接调用warnings模块的showwarning函数来显示警告信息,但这种情况很少见。
showwarning(message, category, filename, lineno, file=None, line=None)
- message 参数
message参数是一个字符串(将作为警告信息的一部分),或者一个警告对象(Warning类或其派生类的实例)。- category 参数
category参数是警告的类型(Warning类或其派生类)。- filename 参数
filename参数表示与警告相关联的脚本文件的路径。- lineno 参数
lineno参数表示与警告相关联的代码的行号。- file 参数
file参数是用于输出警告信息的对象,如果未指定,则showwarning会尝试使用sys.stderr。- line 参数
line参数是与警告相关联的代码,如果未指定,则showwarning会尝试通过filename和lineno参数读取相关代码。
自定义的 showwarning 函数需要自行处理未指定的 file 和 line 参数
warnings模块原有的showwarning函数,会处理参数file和line为空值的情况,如果你定义了自己的showwarning函数,那么需要在函数中完成同样的工作。
在下面的示例中,我们首先调用函数showwarning直接显示了一条警告,然后定义了函数mywarning以自定义警告的显示方式。
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('再次警告')如果你并不想修改整个警告的显示方式,仅希望自定义警告信息中的文字内容,那么可以编写一个新的函数或方法来替代warnings模块原有的formatwarning函数。formatwarning函数与showwarning的参数的含义一致,但由于没有file参数,自定义的formatwarning函数仅需要处理line参数为空值的情况。
formatwarning(message, category, filename, lineno, line=None)
formatwarning 函数由 warnings 模块原有的 showwarning 函数调用
当然,直接调用formatwarning函数将得到包含警告信息的字符串,但这种情况很少见,因为formatwarning函数总是由warnings模块原有的showwarning函数来调用,原有的showwarning函数会将formatwarning函数的返回值写入到file参数表示的 Python 文件对象中。
在下面的示例中,我们使用函数myinfo来自定义警告的文字内容。
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 警告过滤器
事实上,warnings模块的warn函数会根据参数来决定下一步的动作,警告可能会被转换为一个异常,也可能会被忽略。所有的这一切均由警告过滤器来控制,该过滤器是一个 Python 列表,对应了warnings模块的filters变量,列表中包含了一系列的匹配规则,每一个规则均对应一个 Python 元组,其格式为(action,message,category,module,lineno)(元组中的每一项的作用将在下面列出)。当warn函数的参数符合某个匹配规则,即匹配项message,category,module和lineno时,则该匹配规则的action项,将指示warn函数如何进行下一步的操作。
- action 项
action是一个字符串,用来指示warn函数如何处理当前的警告,他拥有以下的有效取值,'default'表示指向相同模块的相同代码的所有同类警告,仅第一个会被显示,'error'表示会将警告作为异常抛出,'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 函数的参数未能与警告过滤器的任何规则匹配时
如果warnings模块的warn函数的参数,未能与警告过滤器的任何规则匹配,那么warn函数接下来进行的操作,会等同于将action项设置为'default'的情况。
警告过滤器的匹配规则可以与不同类的警告匹配
你可以将message参数和警告类型相同的警告视为同类警告,警告过滤器的同一个匹配规则能够与不同类的警告匹配。
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 警告过滤器添加匹配规则
在某些情况下,你可能想为警告过滤器添加匹配规则,以修改一些警告的处理方法,warnings模块的filterwarnings函数可以完成此目标,通过该函数添加的匹配规则,将成为warnings模块的filters列表的第一个元素(可将append参数设置为True,以让新规则被添加至filters列表的末尾),这表示新加入的规则比原有规则的优先级更高。除了append,filterwarnings函数的参数与之前描述的警告过滤器匹配规则中的各项的含义相同。
filterwarnings(action, message='', category=Warning, module='', lineno=0, append=False)
在下面的示例中,我们使用函数filterwarnings让同类的警告只能在同一个模块中显示一次,无论他们指向的代码位于哪一行。
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函数同样可以用于为警告过滤器添加匹配规则,只不过他缺少了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 警告过滤器的所有匹配规则
如果你已经为警告器添加了足够多的匹配规则,现在想要删除他们,那么可以使用warnings模块的resetwarnings函数,该函数会清空警告器中的所有匹配规则,包括默认的匹配规则在内,这会让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 警告过滤器的匹配规则
无论是为警告器添加匹配规则,还是通过resetwarnings清空匹配规则,你都可能希望这是一种临时的改变,尤其是通过添加匹配规则来测试一些警告的时候。使用warnings模块的catch_warnings类和with语句可以完成上述目标,在通过with语句进入与catch_warnings对象相关的上下文之后,对警告过滤器的匹配规则以及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关键字将保存警告的列表绑定至某个变量,这样做的好处在于,with语句中发出的警告不会直接显示,你可以更容易的得知catch_warnings所产生的效果。如果record参数被设置为False,那么保存警告的列表并不会存在。- module 参数
module参数用于表示进入与catch_warnings对象相关的上下文之前的warnings模块,catch_warnings对象将备份该模块的filters(即警告过滤器的匹配规则)和showwarning等特性,以便在with语句结束时恢复他们。如果未指定module参数,那么将使用sys.modules中的warnings模块。一般情况下,module参数仅用于测试warnings模块本身,你可以将自己改写的warnings模块赋值给该参数,以查看其运行效果。- action,category,lineno,append 参数
参数
action,category,lineno,append与函数simplefilter的参数的含义相同,如果action被指定了一个有效的值,那么在进入与catch_warnings对象相关的上下文之后,以上四个参数将用于调用函数simplefilter,这相当于在with语句中为警告过滤器添加了一个匹配规则。
在警告器的默认匹配规则中,类型为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通过命令行参数或环境变量为 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的改动生效。
检查你的代码是否使用了已经弃用的内容
如果未作出任何调整,那么关于弃用内容的警告DeprecationWarning可能不会显示,这是可以理解的,因为在程序被正常使用时,这些警告信息不应该展示给用户。不过,你可以通过添加命令行选项-Wd,-Wdefault或-W default::DeprecationWarning,来确认代码是否使用了已经弃用的内容。
当然,要完成以上目标,设置环境变量PYTHONWARNINGS或直接通过warnings模块修改匹配规则也是可行的。
设置环境变量
关于如何设置环境变量,你可以查看编程指南的如何设置 Windows 环境变量,如何设置 UNIX/Linux/macOS 环境变量两节。
在默认情况下,脚本文件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 抛出异常