Python 中防御性编程的断言和缺点
介绍
要了解并深入了解防御性编程的理论,请查看本系列的第一篇指南。
断言
断言在单元测试中非常常见。事实上,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.调试模式
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
正确使用断言
结论
免责声明:本内容来源于第三方作者授权、网友推荐或互联网整理,旨在为广大用户提供学习与参考之用。所有文本和图片版权归原创网站或作者本人所有,其观点并不代表本站立场。如有任何版权侵犯或转载不当之情况,请与我们取得联系,我们将尽快进行相关处理与修改。感谢您的理解与支持!
请先 登录后发表评论 ~