PEP 230 – 警告框架
- 作者:
- Guido van Rossum <guido at python.org>
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2000-11-28
- Python 版本:
- 2.1
- 历史记录:
- 2000-11-05
摘要
本 PEP 提出了一种 C 和 Python 层级的 API,以及命令行标志,用于发出警告消息并控制对其的处理。这主要基于 GvR 于 2000 年 11 月 5 日发布到 python-dev 的提案,并结合了 Paul Prescod 在同一天发布的反提案中的一些想法(例如使用类对警告进行分类)。此外,尝试实现该提案导致了一些小的调整。
动机
随着 Python 3000 的临近,除了错误之外,还需要开始发出有关使用过时或弃用功能的警告。还有很多其他原因需要能够发出警告,无论是来自 C 代码还是 Python 代码,无论是在编译时还是在运行时。
警告不是致命的,因此程序有可能在单个执行期间多次触发相同的警告。如果程序发出无限的相同警告流,那将很烦人。因此,需要一种机制来抑制多个相同的警告。
还希望用户能够控制打印哪些警告。虽然通常情况下一直查看所有警告都很有用,但在某些情况下,在生产程序中立即修复代码可能不切实际。在这种情况下,应该有一种方法可以抑制警告。
在程序开发期间能够抑制特定的警告也很有用,例如,当警告是由无法立即修复的第三方代码生成时,或者当无法修复代码时(可能为一段完全正常的代码生成了警告消息)。在这种情况下,建议抑制所有警告是不明智的:开发人员会错过有关其余代码的警告。
另一方面,也存在一些可以将某些或所有警告视为错误的情况。例如,本地编码标准可能规定不应使用特定的弃用功能。为了强制执行此规则,可以将有关此特定功能的警告转换为错误,从而引发异常(并非必须将所有警告都转换为错误)。
因此,我建议引入一个灵活的“警告过滤器”,它可以根据以下条件过滤掉警告或将其更改为异常:
- 在代码中的哪个位置生成(按包、模块或函数)
- 警告类别(警告类别将在下面讨论)
- 特定的警告消息
警告过滤器必须可以通过命令行和 Python 代码进行控制。
发出警告的 API
- 要从 Python 中发出警告:
import warnings warnings.warn(message[, category[, stacklevel]])
如果给出,category 参数必须是警告类别类(见下文);默认为 warnings.UserWarning。如果通过警告过滤器将发出的特定警告更改为错误,则这可能会引发异常。stacklevel 可以由用 Python 编写的包装函数使用,如下所示:
def deprecation(message): warn(message, DeprecationWarning, level=2)
这使得警告指向 deprecation() 的调用方,而不是指向 deprecation() 本身(因为后者会破坏警告消息的目的)。
- 要从 C 中发出警告:
int PyErr_Warn(PyObject *category, char *message);
正常情况下返回 0,如果引发异常则返回 1(要么是因为警告已转换为异常,要么是因为实现中的故障,例如内存不足)。category 参数必须是警告类别类(见下文)或
NULL
,在这种情况下,它默认为PyExc_RuntimeWarning
。当PyErr_Warn()
函数返回 1 时,调用方应执行正常的异常处理。PyErr_Warn()
的当前 C 实现导入 warnings 模块(在 Python 中实现)并调用其warn()
函数。这最大程度地减少了需要添加的 C 代码量来实现警告功能。[XXX 未解决问题:在词法分析或解析期间发出警告会怎样,这些操作没有可用的异常机制?]
警告类别
有一些内置异常表示警告类别。这种分类对于能够过滤掉一组警告很有用。当前定义了以下警告类别类:
Warning
– 这是所有警告类别类的基类,它本身是 Exception 的子类UserWarning
–warnings.warn()
的默认类别DeprecationWarning
– 关于弃用功能的警告的基类别SyntaxWarning
– 关于可疑语法功能的警告的基类别RuntimeWarning
– 关于可疑运行时功能的警告的基类别
[XXX:在本次 PEP 的审查期间可能会提出其他警告类别。]
这些标准警告类别在 C 中可用作PyExc_Warning
、PyExc_UserWarning
等。在 Python 中,它们在__builtin__
模块中可用,因此无需导入。
用户代码可以通过对标准警告类别之一进行子类化来定义其他警告类别。警告类别必须始终是 Warning 类的子类。
警告过滤器
警告过滤器控制是否忽略、显示或将警告转换为错误(引发异常)。
警告过滤器有三个方面:
- 用于有效确定特定
warnings.warn()
或PyErr_Warn()
调用的处理方式的数据结构。 - 用于从 Python 源代码控制过滤器的 API。
- 用于控制过滤器的命令行开关。
警告过滤器分几个阶段工作。它针对(预期很常见)的情况进行了优化,即在代码中的同一位置反复发出相同的警告。
首先,警告过滤器收集发出警告的模块和行号;此信息可以通过sys._getframe()
轻松获取。
从概念上讲,警告过滤器维护一个有序的过滤器规范列表;任何特定的警告都会依次与列表中的每个过滤器规范进行匹配,直到找到匹配项;匹配项确定匹配项的处理方式。每个条目都是一个元组,如下所示:
(category, message, module, lineno, action)
- category 是一个类(
warnings.Warning
的子类),警告类别必须是其子类才能匹配 - message 是一个已编译的正则表达式,警告消息必须与其匹配(匹配不区分大小写)
- module 是一个已编译的正则表达式,模块名称必须与其匹配
- lineno 是一个整数,警告发生的行号必须与其匹配,或 0 以匹配所有行号
- action 是以下字符串之一:
- “error” – 将匹配的警告转换为异常
- “ignore” – 从不打印匹配的警告
- “always” – 始终打印匹配的警告
- “default” – 打印每个发出警告的位置的匹配警告的第一次出现
- “module” – 打印每个发出警告的模块的匹配警告的第一次出现
- “once” – 只打印匹配警告的第一次出现
由于Warning
类派生自内置Exception
类,因此要将警告转换为错误,我们只需引发category(message)
。
警告输出和格式化钩子
当警告过滤器决定发出警告时(但不是当它决定引发异常时),它会将有关函数warnings.showwarning(message, category, filename, lineno)
的信息传递给它。此函数的默认实现将警告文本写入sys.stderr
,并显示 filename 的源代码行。它有一个可选的第 5 个参数,可用于指定与sys.stderr
不同的文件。
警告的格式由一个单独的函数warnings.formatwarning(message, category, filename, lineno)
处理。这将返回一个字符串(可能包含换行符并在换行符处结束),可以将其打印出来以获得与showwarning()
函数完全相同的效果。
操作警告过滤器的 API
warnings.filterwarnings(message, category, module, lineno, action)
这检查参数的类型,编译消息和模块正则表达式,并将它们作为元组插入到警告过滤器的前面。
warnings.resetwarnings()
将警告过滤器重置为空。
命令行语法
应该有命令行选项来指定最常见的过滤操作,我希望至少包括:
- 抑制所有警告
- 在任何地方抑制特定警告消息
- 抑制特定模块中的所有警告
- 将所有警告转换为异常
我建议使用以下命令行选项语法:
-Waction[:message[:category[:module[:lineno]]]]
其中:
- “action” 是允许的操作之一的缩写(“error”、“default”、“ignore”、“always”、“once”或“module”)
- “message” 是一个消息字符串;匹配其消息文本是“message” 的初始子字符串的警告(匹配不区分大小写)
- “category” 是标准警告类别类名称的缩写或用户定义警告类别类的完全限定名称,格式为 [package.]module.classname
- “module” 是一个模块名称(可能是 package.module)
- “lineno” 是一个整数行号
除了“action” 之外,所有部分都可以省略,其中去除空格后的空值与省略值相同。
解析 Python 命令行的 C 代码将所有 -W 选项的主体保存在一个字符串列表中,该列表作为 sys.warnoptions 提供给 warnings 模块。warnings 模块在第一次导入时会解析这些选项。在解析 sys.warnoptions 期间检测到的错误不是致命的;一条消息将写入 sys.stderr,并且处理将继续进行此选项。
示例
-Werror
- 将所有警告转换为错误
-Wall
- 显示所有警告
-Wignore
- 忽略所有警告
-Wi:hello
- 忽略其消息文本以“hello”开头的警告
-We::Deprecation
- 将弃用警告转换为错误
-Wi:::spam:10
- 忽略模块 spam 第 10 行的所有警告
-Wi:::spam -Wd:::spam:10
- 忽略模块spam中的所有警告,除了第10行。
-We::Deprecation -Wd::Deprecation:spam
- 将弃用警告转换为错误,除了模块spam。
未解决的问题
一些我想到的未解决问题
- 在词法分析或解析期间发出警告怎么办,这些警告没有可用的异常机制?
- 提议的命令行语法有点丑陋(尽管简单的情况还不错:
-Werror
、-Wignore
等)。有没有更好的想法? - 我有点担心过滤器规范过于复杂。也许只根据类别和模块(而不是根据消息文本和行号)进行过滤就足够了?
- 模块名称和文件名之间存在一些混淆。报告使用文件名,但过滤器规范使用模块名称。也许它也应该允许文件名?
- 我完全不相信包的处理方式是正确的。
- 我们需要更多标准的警告类别吗?还是更少?
- 为了最大程度地减少启动开销,警告模块由对
PyErr_Warn()
的第一次调用导入。它在导入时对-W
选项进行命令行解析。因此,无警告程序可能不会抱怨无效的-W
选项。
被拒绝的顾虑
Paul Prescod、Barry Warsaw和Fred Drake提出了其他一些我认为不重要的担忧。我在这里解决它们(这些担忧是释义的,并非完全是他们的原话)
- Paul:
warn()
应该是一个内置函数或语句,以便于使用。回复:“from warnings import warn” 足够简单。
- Paul:如果我有一个速度关键的模块在内部循环中触发警告怎么办。应该可以禁用检测警告的开销(不仅仅是抑制警告)。
回复:重写内部循环以避免触发警告。
- Paul:如果我想查看警告的完整上下文怎么办?
回复:使用
-Werror
将其转换为异常。 - Paul:我更喜欢“:*:*:”而不是“:::”来省略警告规范的部分内容。
回复:我不喜欢。
- Barry:如果行号可以是范围规范就好了。
回复:已经太复杂了。
- Barry:我想添加我自己的警告操作。也许如果“action”可以是可调用对象以及字符串。然后在我的IDE中,我可以将其设置为“mygui.popupWarningsDialog”。
回复:为此,您将覆盖
warnings.showwarning()
。 - Fred:为什么警告类别类必须在
__builtin__
中?回复:鉴于警告类别必须在第一个
PyErr_Warn()
调用(导入警告模块)之前在C中可用,这是最简单的实现。我认为将它们作为内置函数没有问题。
实现
这是一个原型实现:http://sourceforge.net/patch/?func=detailpatch&patch_id=102715&group_id=5470
来源:https://github.com/python/peps/blob/main/peps/pep-0230.rst
上次修改时间:2023-09-09 17:39:29 GMT