Python 中防御性编程的断言和缺点

发布日期:2026-07-20 00:49:03  来源 : 杭州电子商务研究院    浏览量 :0
杭州电子商务研究院 发布日期:2026-07-20 00:49:03  
0

介绍

要了解并深入了解防御性编程的理论,请查看本系列的第一篇指南

断言

断言在单元测试中非常常见。事实上,Python有大量针对单元测试的自定义断言集合。但是,没有理由只在测试世界中使用这个有用的工具。

普通代码中的断言语句也非常有用。这些语句接受一个表达式并引发AssertionError,如果表达式为False ,还会引发一条可选消息。

假设我们有以下函数,它从用户那里获取值并将指定范围的数据规范化为 0 到 1 之间的某个值,以便以后的新小部件可以使用它。

      def normalize_ranges(colname):
    """
    Normalize given data range to values in [0 - 1]

    Return dictionary new 'min' and 'max' keys in range [0 - 1]
    """

    # 1-D numpy array of data we loaded application with
    original_range = get_base_range(colname)
    colspan = original_range['datamax'] - original_range['datamin']

    # User filtered data from GUI
    live_data = get_column_data(colname)
    live_min = numpy.min(live_data)
    live_max = numpy.max(live_data)

    ratio = {}
    try:
        ratio['min'] = (live_min - original_range['datamin']) / colspan
        ratio['max'] = (live_max - original_range['datamin']) / colspan
    except ZeroDivisionError:
        ratio['min'] = 0.0
        ratio['max'] = 0.0

    return ratio
    

如果我们上面的函数声称总是返回0 - 1之间的值。不幸的是,进一步强调我们的假设表明事实并非如此:

      >>> age = numpy.array([-10.0, 20.0, 30.0, 40.0, 50.0])
>>> normalize_ranges('age')
{'max': 1.0, 'min': -0.5}
    

可以想象,这种情况很容易在很长一段时间内被忽视,并且此返回值可能会传播到整个代码库。这正是无法找到的错误类型,并导致了本系列指南开头的悲惨故事。

我们可以尝试考虑用户可能传递的每个值并正确处理它。事实上,这是正确的做法,但不能保证我们不会错过任何东西。我们已经承认程序员是会犯错的。

幸运的是,既然我们已经接受了我们会犯错误的事实,我们就可以使用断言语句来对付我们未来的自己。

      def normalize_ranges(colname):
    """
    Normalize given data range to values in [0 - 1]

    Return dictionary new 'min' and 'max' keys in range [0 - 1]
    """

    # 1-D numpy array of data we loaded application with
    original_range = get_base_range(colname)
    colspan = original_range['datamax'] - original_range['datamin']

    # User filtered data from GUI
    live_data = get_column_data(colname)
    live_min = numpy.min(live_data)
    live_max = numpy.max(live_data)

    ratio = {}

    try:
        ratio['min'] = (live_min - original_range['datamin']) / colspan
        ratio['max'] = (live_max - original_range['datamin']) / colspan
    except ZeroDivisionError:
        ratio['min'] = 0.0
        ratio['max'] = 0.0

    assert 0.0 <= ratio['min'] <= 1.0, (
            '"%s" min (%f) not in [0-1] given (%f) colspan (%f)' % (
            colname, ratio['min'], original_range['datamin'], colspan))

    assert 0.0 <= ratio['max'] <= 1.0, (
            '"%s" max (%f) not in [0-1] given (%f) colspan (%f)' % (
            colname, ratio['max'], original_range['datamax'], colspan))

    return ratio
    

我们添加了一些断言语句,如果我们没有返回预期范围内的值,这些语句会提醒我们。让我们看看这些断言如何改变我们的小测试用例:

      >>> age = numpy.array([-10.0, 20.0, 30.0, 40.0, 50.0])
>>> normalize_ranges('age')
AssertionError: "age" min (-0.500000) not in [0-1] given (10.000000) colspan(40.000000)
    

这个小改变有几个好处:

  • 作为一种可执行文档的形式
  • 将警告置于更接近根本问题的位置
  • 包含有关“无效”参数的宝贵调试信息

1. 作为可执行文档的一种形式

通常,文档有几种不同的形式,例如内联注释或块注释、文档字符串和 Sphinx。每种形式都有特定的用途,并且对软件开发几乎必不可少。不幸的是,它们都存在相同的问题:它们很快就会与快速变化的代码和需求脱节。这导致开发人员无法信任文档。

断言充当了具有不同目的的文档。它们清晰简洁地描述了应用程序在运行时的预期状态。此外,如果我们更改假设而不修改断言以匹配新行为,应用程序会发出抱怨。

断言语句更有可能与其他更改一起更新。因此,断言比不可执行的文档更值得信赖。此外,断言仍然提供注释、文档字符串等的许多好处。

值得一提的是,Python 生态系统中还有另一种相当常见的可执行文档形式,称为doctests。这些测试/文档看起来可能有些丑陋,但它们的主要特点是它们接近代码,就像断言一样。

2. 将警告置于更接近问题根源的位置

我们都经历过这种情况,你花了几个小时调试一个问题,然后才意识到真正的错误甚至离你开始的地方还很远(参见5 个为什么)。也许错误的根本原因在逻辑上离你第一次看到症状的地方很远。

例如,您在系统深处发现了一个字节字符串,但您假设内部所有内容都是 Unicode 字符串。可能需要很长时间才能找到转换首次中断的位置。这是一种令人沮丧的情况。如果能早点发现错误或至少有更多的调试信息就好了。

断言不会阻止这种情况,但它们确实提供了改进它的机会。上面的断言将在该函数不遵守其契约返回0 - 1之间的值时提醒我们。如果我们发现其他具有无效范围的代码,这可能会在以后给我们提供有价值的线索。我们会知道这个函数没有履行其契约的义务这个线索实际上可以节省数小时,因为它可以避免从症状一直追溯到原因。

3. 包含有关“无效”参数的宝贵调试信息

请注意,我们的断言语句还包含有关输入参数的信息。当用户使用我们无法访问的数据遇到错误时,这些信息将非常有价值。此外,当用户难以解释错误场景时,调试信息将特别有用。因此,这几个断言语句可以防止您成为可耻地将错误标记为“不可重现”状态的人。

输入参数信息还有其他一些微妙的好处:

  • 显示有关用户正在运行的数据类型的无效假设。
  • 解释我们的文档中关于预期什么类型的数据的疏忽。
  • 暴露无法执行的潜在新用例。

断言缺点

我们已经确定断言可以带来很多好处,但它并不全是乐趣和游戏。像往常一样,它也有缺点。

1.调试模式

通常,出于技术和实际原因,断言语句不适用于生产代码。仅当隐藏的调试常量为True时,断言才会启用。但是,此常量的默认值为True,这意味着您的代码很可能当前处于调试模式。

如果您的应用程序处于可以注意到少量额外逻辑的环境中,则需要考虑这一点。关闭调试模式的唯一方法是使用-O 选项运行 Python 解释器

2. 代码噪音增加

过度使用断言很容易,很快就会使你的代码难以阅读。这会使得你的代码非常嘈杂,并将真正的功能埋没在一系列错误检查和条件中。以下代码就是过度使用的示例,很难看出代码的用途。

      def normalize_ranges(colname):
    """
    Normalize given data range to values in [0 - 1]

    Return dictionary new 'min' and 'max' keys in range [0 - 1]
    """

    assert isinstance(colname, str)

    original_range = get_base_range(colname)
    assert original_range['datamin'] >= 0
    assert original_range['datamax'] >= 0
    assert original_range['datamin'] <= original_range['datamax']

    colspan = original_range['datamax'] - original_range['datamin']
    assert colspan >= 0, 'Colspan (%f) is negative' % (colspan)

    live_data = get_column_data(colname)
    assert len(live_data), 'Empty live data'

    live_min = numpy.min(live_data)
    live_max = numpy.max(live_data)

    ratio = {}

    try:
        ratio['min'] = (live_min - original_range['datamin']) / colspan
        ratio['max'] = (live_max - original_range['datamin']) / colspan
    except ZeroDivisionError:
        ratio['min'] = 0.0
        ratio['max'] = 0.0

    assert 0.0 <= ratio['min'] <= 1.0
    assert 0.0 <= ratio['max'] <= 1.0

    return ratio
    

正确使用断言

谨慎使用断言,只用于那些你认为永远不会发生的事情。不要过度使用断言来检查无效输入。

对此没有硬性规定,每个开发人员对断言使用的容忍度可能不同。尝试采用一些自己的标准并将其包含在开发人员风格指南中。

您确实有风格指南,对吧?

另外,请记住 Python 支持鸭子类型,所以不要过度使用断言来验证所有类型,从而破坏这一点。

我发现一种有用的技术是在应用程序的顶层捕获所有AssertionError异常,并将它们与另一种有用的技术结合起来。

结论

这种开发风格很难分类,不幸的是,没有任何固定的规则来说明何时使用什么。所以,我鼓励你记住这些指导方针。继续阅读本系列的下一篇指南,了解有关防御性编程日志记录的更多信息。

这些指导方针将导致思维方式发生微妙的变化。思维方式的改变很重要,而不是工具和机制本身。最终,你会因为过度使用断言日志而犯一些错误,并开始形成自己的风格。此外,每个项目的要求都不同,因此学习所有工具并以适合你情况的方式组合它们很重要。

以上内容来自杭州电子商务研究院推送
分享到:

长按或扫码识别 分享给好友

长按或扫码识别 分享给好友
关于我们
热门推荐
合作伙伴
免责声明:本站部分资讯来源于网络,如有侵权请及时联系客服,我们将尽快处理
Copyright © 2025-2027 ToB产业网址导航 公安备案 浙公网安备33010602013138号 浙ICP备16025413号-9
支持 反馈 关注 数据