四时宝库

程序员的知识宝库

在Python中处理警告(python 警告)

我们每个人都会遇到这种情况: 你写了一些Python代码,但是你遇到了一个错误:

这不仅仅是一个错误,而是一个异常。这是Python以明确的方式表述存在问题的方式,这样,我们就可以用“try”和“except”关键字来捕获它。

就像Python中的其他东西一样,异常也是一个对象。这意味着一个异常有一个类——我们就是用这个类来捕获异常的:

我们甚至可以有几个“except”子句,每个子句会寻找一种不同类型的错误。但是,每个Python类(除了“object”)都继承自其他类,对于异常类也是如此。因此,如果我们想同时捕获“KeyError”和“IndexError”,那么我们可以显式地命名它们。或者我们可以只捕获“LookupError”,它是“KeyError”和“IndexError”的父类。

在Python标准库文档中,你可以在https://docs.python.org/3/library/exceptions.html 中查看Python的异常类层次结构。它有助于你了解Python中存在哪些异常,层次结构如何,以及一般地理解异常如何工作。

但是,如果你查看该层次结构的底部,你将看到一个名为“Warning”的异常类,以及一些子类,比如“DeprecationWarning”和“BytesWarning”。这些是什么?虽然它们与异常层次结构一起被包含在其中,警告也是异常,但它们既不会像正常的异常那样被抛出,也不会像正常的异常那样被使用。它们是什么,我们要如何使用它们?

首先,回顾一下历史: Warnings在Python中已经存在很长时间了,从Python 2.1开始(追溯到2000年)。PEP 230是由Guido van Rossum (Python的创造者和长期的“仁慈的生活的独裁者”)编写的,它的添加不仅是为了创建一种机制来提醒用户可能出现的问题,而且是为了从程序内部发送此类警告并决定如何处理它们。

为什么要用警告?

在我向你展示如何在你自己的代码中使用警告之前,让我们首先考虑一下为什么以及何时需要使用警告。毕竟,你总是可以使用“print”来显示警告。或者,如果确实有问题,那么你也可以抛出一个异常。

但这正是问题的关键:在某些时候,你想要吸引用户的注意,但又不需要停止程序或强制执行try-except子句。虽然“print”总是很有用,但它通常会写入标准输出(也就是Python中的“sys.stdout”),这意味着你的警告可能会与系统的警告本身混合在一起。

(虽然我刚写过,你可能会想要引起用户的注意,但我认为,大多数情况下,警告是针对开发人员而不是用户的。Python中的警告有点像汽车上的“所需的服务”灯;用户可能知道有些地方出了问题,但是只有一个经过认证的维修人员才会知道该怎么做。开发人员应该避免向最终用户显示警告。)

你还可以想象这样一种情况,在这种情况下,某些警告比其他警告更重要。你当然可以设计一种方案,在该方案中,程序会“print”警告,并且该警告的第一个字符将指示警告的严重程度……但是,在Python有一个完整的对象系统,以及可以自由使用的复杂数据类型的情况下,我们为什么要这样做呢?

此外,在某些情况下,用户可能不希望忽略警告。也许我在生产环境中运行的是一种非常敏感的程序,我宁愿让程序提前退出,也不愿在一个潜在的不确定情况下继续运行。

Python的警告系统考虑到了这一切:

  • 它将警告看作一个单独的输出类型 , 这样我们就不会将它与异常或者程序的打印文本相混淆。

  • 它允许我们指明我们正在发送给用户哪种警告,

  • 它可以让用户指示如何处理不同类型的警告,让一些引发严重错误,其他的在屏幕上显示它们的信息,还有一些始终被忽略,

  • 它可以让程序员开发它们自己的、新的警告类型。

并不是每个程序都需要有或使用警告。但是你的程序可能会因为加载模块或调用函数的方式而责骂用户,因此,Python的警告系统就为你提供了你想要的功能。

警告用户

假设你想要警告用户某些事情。你可以通过导入“warnings”模块来实现这一点,然后使用“warnings.warn”来告诉他们出现了什么问题:

当我运行上面的代码(在一个名为“warnings1.py”的文件中)时,会发生什么呢?输出如下:

换句话说,上面的内容都被写到了我的终端屏幕上。这三行代码都是按顺序打印的,所以警告并不是在程序的单独阶段(例如,编译)被打印的。但是在“print”语句的文本和我从警告中得到的输出之间有一个明显的区别。

首先,我们被告知警告发生在哪个文件中,在哪一行。在这样一个小而琐碎的例子中,这似乎有些过分。但是,如果你有一个包含许多不同文件的大型应用程序,那么知道是什么代码生成了警告无疑是很好的。

我们还被告知这是一个“UserWarning”—我们可以生成的警告类型之一。正如不同类型的异常允许我们选择性地捕获它们一样,不同类型的警告也允许我们以不同的方式处理它们。

但是在这个输出中还有一些隐藏的东西:“print”语句和我的“warnings.warn”语句实际上将它们的输出发送到了两个不同的地方。正如我上面写的,“print”通常会写到“标准输出”,也就是“sys.stdout”,它通常会连接到用户的终端窗口。但“warnings.warn”通常会写到“标准错误”,也就是“sys.stderr”。问题是,在默认情况下,“sys.stdout” 和 “sys.stderr”都会写到同一个地方,即用户的终端。

但是如果我把程序输出重定向到一个文件中,我们来看看会发生什么:

我告诉我的Unix shell我想运行“warnings1.py”,所有的输出都应该被放在“output.txt”中,而不是显示在屏幕上。但我并没有说“全部输出”。相反,通过使用“>”,我只重定向了发送到“sys.stdout”的输出。被发送到“sys.stderr”的警告仍然被显示出来了。这通常被认为是一件好事,它可以确保即使你将输出重定向到一个文件,你仍然能够看到警告和其他错误。因此,尽管sys.stdout 和 sys.stderr 在默认情况下都会去同一个目的地,但我们也可以看到将它们分开的好处。

不同类型的警告

假设我正在维护一个已经存在了一段时间的库。这个库有一个有用的函数,但是这个函数有点过时了,并且不支持现代的用例。作为该库的维护者,支持该函数的两个版本(旧版本和新版本)对我来说是一件痛苦的事情。

我可以在文档和社交媒体中声明,我的库的新版本(3.0)将于明年释出,而且这个新版本将不再支持该函数的旧版本。但是我们都知道程序员并不倾向于阅读文档。因此,我更愿意让用户感到震惊,告诉他们虽然旧的函数版本仍然可以运行,但他们应该开始转向更新的版本。

我该怎么做呢?当然是使用警告!下面是一个例子:

现在,只要用户运行了“hello”函数,他们就会得到一个警告。此外,因为这个警告会写到标准错误(而不是标准输出),所以它不会与正常输出混在一起。下面是来自上面代码的输出:

但还有比这更好的方法:或许我们想把普通的、乏味的警告与其他类型的警告区分开来。例如,我们可能有许多被废弃的函数。为了处理这中情况,“warnings.warn”函数支持可选的第二个参数——警告的类别。例如,我们可以使用DeprecationWarning:

我们不必“import”DeprecationWarning或任何其他的标准警告类型,因为它们已经被自动导入到了Python程序始终可用的“内置”命名空间中。这样一来,就有许多这样的警告类可以供我们使用,包括UserWarning(默认)、DeprecationWarning(我们在这里使用了)、SyntaxWarning和UnicodeWarning。你可以使用其中你认为最合适的一个。

你可能已经注意到,这些警告类别与我们在前面查看Python的内置异常层次结构时所看到的类完全相同。实际上,这些类就是这样被使用的,作为第二个参数传递给“warnings.warn”。

简单的过滤

假设你正在使用一堆旧函数,每一个函数都会提醒你,你应该切换到它们的新的替代版本。如果每次运行程序时都收到一堆警告,你可能会有点恼火。警告是用来通知你你应该进行升级……但是有时候,这些警告与其说有用,不如说是烦人。

在这种情况下,你可能希望过滤掉一些警告。现在,“filtering”是警告系统使用的一个非常普遍的术语。它基本上是让你说,“当一个匹配特定条件的警告触发时,使用它做X”——X可以是各种各样的事情。

最简单的过滤器是“warnings.simplefilter”,而调用它的最简单方法是使用单个字符串参数。这个参数告诉警告系统,如果它遇到一个警告,该怎么做:

  • “默认”——在警告第一次出现时显示它

  • “错误”——将警告转换成一个异常

  • “忽略”——忽略警告

  • “总是”——总是显示警告,即使它以前被显示过

  • “模块”——每个模块显示一次警告

  • “一次”——在整个程序中只显示一次警告

例如,如果我想忽略所有警告,我可以这样写:

并且如果我有这样的代码:

我们会看到这样的输出:

如你所见,由于使用了“ignore”,警告完全消失了。

如果我们采取另一种极端,即将警告转换为异常,会发生什么?

果然,我们得到了一个异常:

正如你所看到的,我们得到了一个UserWarning异常。我们可以在这些警告上面使用“try”和“except”,如果我们想的话,我们就可以捕获它们……尽管我必须承认,在我看来,将警告转换成异常只是为了捕获它们是很奇怪的。(不过,我相信有这样的用例。)

更具体的过滤

我提到过“simplefilter”采用了一个强制性的参数,并且我们已经看到了这些参数可以是什么。但事实证明,“simplefilter”使用了几个额外的、可选的参数,这些参数可用于指定一个警告被发出时将发生什么。

例如,假设我想忽略UserWarning,并将DeprecationWarning转换为异常。我可以这样写:

这段代码会生成以下输出:

换句话说,我们成功地忽略了一种类型的警告,同时将另一种类型的警告转换为异常——与所有异常一样,如果忽略这种异常,它将是致命的。

“simplefilter”函数接受四个参数,除了第一个参数外,其他参数都是可选的

你还能做什么?

警告系统可以处理非常广泛的各种情况,并且可以通过多种方式进行配置。除此之外,你还可以:

  • 使用-W标志从命令行定义警告过滤器

  • 设置多个过滤器,每个过滤器处理一种不同的情况

  • 指定应该过滤的消息和模块,可以以一个字符串,也可以以一个正则表达式

  • 创建你自己的警告,作为现有警告类的子类

  • 使用Python的日志模块捕获警告,而不是将输出打印到sys.stderr。

  • 将输出传递到一个你选择的可调用对象(即函数或类),而不是sys.stderr,用于更高级的处理。

  • 关于警告的原始文档(PEP 230)是一个很好的起点,它还描述了引入警告的动机。

  • 标准库中的“warnings”模块文档描述了我在这里所写的所有内容,以及更多的内容。

  • “本周Python模块”网站有一个很好的介绍:https://pymotw.com/3/warnings/


英文原文:https://lerner.co.il/2020/04/27/working-with-warnings-in-python/
译者:天天向上

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接