Python 警告,警告过滤器介绍,以及发出和过滤 Python 警告

我被代码海扁署名-非商业-禁演绎
阅读 29:30·字数 8853·发布 
Bilibili 空间
关注 960

前提

阅读本节的前提是已经了解 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/exceptions/warnings·codebeatme/python·GitHub