14.4 使用warnings模块

    面向对象开发往往需要对类或模块进行重大的重构。当我们第1次编写应用程序时,很难保证API是完全正确的。事实上,在设计中为了确保API完全正确所花费的时间可能是浪费的:当我们更深入地了解了问题域和用户的需求之后,Python的灵活性允许大规模地修改现有程序。

    我们可以用来支持设计演化过程的其中一个工具就是warnings模块。对于warnings模块,有两个明显的和一个模糊的用例。

    • 用于提醒开发者API的变化,通常用于废弃的或者即将废弃的功能。默认情况下,废弃和即将废弃的警告不会出现。当运行unittest模块时,这些消息不会被隐藏,这可以帮助确保我们在正确使用最新的包。
    • 用于提醒用户配置有问题。例如,某个模块可能有一些可选实现,当最佳的实现不可用时,我们希望可以警告用户当前没有在使用最佳的实现。
    • 我们可能会通过警告用户计算结果中可能包含其他问题的方式打破极限。我们的应用程序可以运行的范围是很模糊的。

    对于前两种情况,我们通常会使用Python的warnings模块显示有一些可以修复的问题。对于第3种情况,我们可能会用logger.warn()方法警告用户有一些潜在的问题。对于这种潜在问题的情况,我们不应该依赖于warnings模块,因为默认情况下,警告信息只会显示一次。

    在一个应用程序中,我们可能会看到下面这些行为。

    • 理想情况下,应用程序正常结束并且完成所有工作,这样的结果确定是有效的。
    • 应用程序生成了一些警告信息,但是仍然正常结束,这些警告信息意味着结果不可信。所有的输出文件都是可读的,但是这些输出的质量和完整性值得怀疑。这种情况可能会让用户感到迷惑,在下面的部分中,我们会围绕这些特殊的不确定性来展示软件设计中一些潜在的问题。
    • 应用程序可能生成了一些错误信息,但是仍然正常结束了。很明显,这样的结果肯定是错误的并且不应该被用于除调试以外的任何场景。logging模块可以帮助我们再细分这些错误。一个产生了错误的程序可能仍然可以正常结束。我们通常用CRITICAL(或者FATAL)来指出Python程序可能没有正确终止,而且输出的文件可能有损坏。我们通常将CRITICAL保留给顶层的try:块使用。
    • 程序可能会在操作系统级别上崩溃。在这种情况下,Python的异常处理和日志系统可能不会生成任何消息。同样地,这种情况下生成的结果是不可用的。

    第2种情况中的值得怀疑的结果不是一个好设计。使用警告——不管是用warnings模块还是logging中的WARN信息——对用户并没有太大帮助。

    14.4.1 用警告信息显示API变化

    当我们改变某个模块、包或者类的API时,可以用warnings模块提供一个方便的标记。这会在已经废弃的或者即将废弃的方法中抛出一个警告信息。

    import warnings
    class Player:
      version= "2.2"
      def bet( self ):
        warnings.warn( "bet is deprecated, use place_bet",
         DeprecationWarning, stacklevel=2 )
        etc.

    当我们这么做之后,任何调用Player.bet()的应用程序都会收到DeprecationWarning。默认情况下,这种警告信息不会显示。但是,我们可以通过调整warnings的过滤器来显示信息,代码如下所示。

    >>> warnings.simplefilter("always", category=DeprecationWarning)
    >>> p2= Player()
    >>> p2.bet()
    main:4: DeprecationWarning: bet is deprecated, use place_bet

    这种技术让我们可以定位应用程序中所有因为API的改变而需要一起改变的地方。如果单元测试覆盖率接近100%,使用这种简单的技术可能可以找出所有使用了废弃方法的地方。

    由于这种警告信息对于计划和管理软件变更很有价值,因此我们有3种方式用于确保我们会看到应用程序中的所有警告信息。

    • 命令行中的-Wd选项会将所有警告的action设置为default。这会启用普通的废弃警告。当我们运行python3.3 –Wd时,我们会看到所有的废弃警告。
    • 使用总是在warnings.simplefilter('default')模式下执行的unittest模块。
    • 在我们的程序中包含warnings.simplefilter('default')。这会将default应用到所有的警告中,与-Wd命令行选项相同。

    14.4.2 用警告信息显示配置问题

    对于某个类或者模块,我们可能会提供多种实现。我们通常会用一个配置文件参数来决定哪种实现是适合的。关于这种技术的更多细节,参见第13章“配置文件和持久化”。

    但是,在一些情况下,应用程序会默认依赖于其他的一些包是否是Python安装程序的一部分。其中一种实现是最优的,另外一种实现可能是备用计划。一种常用的技术是尝试多个import的备选包来定位某个已安装的包。当配置可能存在问题时,我们可以通过生成警告信息的方式来显示这些问题。下面是管理这种可选的一种实现方法。

    import warnings
    try:
      import simulation_model_1 as model
    except ImportError as e:
      warnings.warn( e )
    if 'model' not in globals():
      try:
        import simulation_model_2 as model
      except ImportError as e:
        warnings.warn( e )
    if 'model' not in globals():
      raise ImportError( "Missing simulation_model_1 and simulation_model_2" )

    我们尝试一个模块执行一次导入。如果尝试失败,我们会尝试导入另一个模块。我们用if 语句来减少内嵌的异常。如果选择多于两种,内嵌的异常会形成一个看起来非常复杂的异常。通过使用额外的if语句,我们可以平行化一系列的候选项,这样就不会有内嵌的异常了。

    我们可以通过改变消息的类型来更好地管理这种警告信息。在前面的代码中,这就是UserWarning。这些信息默认都会显示,这样就可以向用户证明现在的配置不是最优的。

    如果我们将类型改变为ImportWarning,默认情况下就不会显示警告信息。当选择不同的包不会对用户造成影响时,这种类型提供了一种通用的不显示警告的操作。运行-Wd选项会显示ImportWarning消息,这是一种典型的开发人员会用到的技术。

    我们可以通过改变调用warnings.warn()的方式来改变警告的类型。

    warnings.warn( e, ImportWarning )

    这行代码把警告的类型改为默认不显示。对于使用-Wd选项的开发人员,这些信息仍然是可见的。

    14.4.3 用警告信息显示可能存在的软件问题

    针对最终用户设计警告信息的想法有一些让人费解:应用程序到底是正常工作还是有问题?这些警告信息意味着什么?用户哪些地方操作不当?

    由于这种潜在的歧义,在用户界面显示警告信息不是一个好主意。要使警告信息真正有用,程序应该正常工作或者完全无法工作。当错误发生时,错误信息应该包含对用户处理该问题的建议。我们不应该强迫用户能够判断输出的质量并决定这些输出是否适用,这一点是需要强调的。

    程序应该正常工作或者完全无法工作。

    一个可能不会引起歧义的有关用户警告信息的用法是警告用户输出是不完整的。例如,应用程序可能无法建立完整的网络连接。基本的结果是正确的,但是其中一个数据源无法正常工作。

    在某些情况下,应用程序执行的操作不是用户所请求的,但是结果仍然是正确可用的。在网络问题的例子中,程序会使用一个默认的行为来代替基于网络资源的行为。通常,用一些正确的但不完全符合用户所请求的来替代失败是一种很好的警告方式。这种警告最好通过loggingWARN级别完成,而不是warnings模块。warnings模块生成的信息只显示一次,但是我们可能希望向用户提供更多的细节。下面是我们如何使用一个简单的Logger.warn()在日志中描述问题。

    try:
      with urllib.request.urlopen("http://host/resource/", timeout= 30 ) as resource:
        content= json.load(resource)
    except socket.timeout as e:
      self.log.warn("Missing information from http://host/resource")
      content= []

    如果发生超时,警告信息会写入日志中,但是程序仍然继续运行。资源的内容会被设置为一个空列表,日志信息每次都会写入。对于程序中的某个给定位置,warnings模块的警告通常只显示一次,之后就不会再显示。