Python 用断言的使用
Python 用断言的使用,有时,真正有用的语言特性得到的关注反而不多,比如内置的assert
语句就没有受到重视。
本文将介绍如何在中使用断言。你将学习用断言来自动检测程序中的错误,让程序更可靠且更易于调试。
读到这里,你可能想知道什么是断言,以及它到底有什么好处。下面就来一一揭晓答案。
从根本上来说,的断言语句是一种调试工具,用来测试某个断言条件。如果断言条件为真,则程序将继续正常执行;但如果条件为假,则会引发AssertionError
异常并显示相关的错误消息。
用断言的使用 示例
下面举一个断言能派上用场的简单例子。本书中的例子会尝试结合你可能在实际工作中遇到的问题。
假设你需要用构建在线商店。为了添加打折优惠券的功能,你编写了下面这个apply_discount
函数:
def apply_discount(product, discount):
price = int(product['price'] * (1.0 - discount))
assert 0 <= price <= product['price']
return price
注意到assert
语句了吗?这条语句确保在任何情况下,通过该函数计算的折后价不低于0,也不会高于产品原价。
来看看调用该函数能否正确计算折后价。在这个例子中,商店中的产品用普通的字典表示。这样能够很好地演示断言的使用方法,当然实际的应用程序可能不会这么做。下面先创建一个示例产品,即一双价格为149美元的漂亮鞋子:
>>> shoes = {'name': 'Fancy Shoes', 'price': 14900}
顺便说一下,这里使用整数来表示以分为单位的价格,以此来避免货币的舍入问题。一般而言,这是个好办法……好吧,有点扯远了。现在如果为这双鞋打七五折,即优惠了25%,则售价变为111.75美元:
>>> apply_discount(shoes, 0.25)
11175
嗯,还不错。接着再尝试使用一些无效的折扣,比如200%的“折扣”会让商家向顾客付钱:
>>> apply_discount(shoes, 2.0)
Traceback (most recent call last):
File "<input>", line 1, in <module>
apply_discount(prod, 2.0)
File "<input>", line 4, in apply_discount
assert 0 <= price <= product['price']
AssertionError
从上面可以看到,当尝试使用无效的折扣时,程序会停止并触发一个AssertionError
。发生这种情况是因为200%的折扣违反了在apply_discount
函数中设置的断言条件。
从异常栈跟踪信息中还能得知断言验证失败的具体位置。如果你(或者团队中的另一个开发人员)在测试在线商店时遇到这些错误,那么查看异常回溯就可以轻松地了解是哪里出了问题。
这极大地加快了调试工作的速度,并且长远看来,程序也更易于维护。朋友们,这就是断言的力量。
用断言的使用 为什么不用普通的异常来处理
你可能很奇怪为什么不在前面的示例中使用if
语句和异常。
要知道,断言是为了告诉开发人员程序中发生了不可恢复的错误。对于可以预料的错误(如未找到相关文件),用户可以予以纠正或重试,断言并不是为此而生的。
断言用于程序内部自检,如声明一些代码中不可能出现的条件。如果触发了某个条件,即意味着程序中存在相应的bug。
如果程序没有bug,那么这些断言条件永远不会触发。但如果违反了断言条件,程序就会崩溃并报告断言错误,告诉开发人员究竟违反了哪个“不可能”的情况。这样可以更轻松地追踪和修复程序中的bug。我喜欢能让生活变轻松的东西,你也是吧?
现在请记住,的断言语句是一种调试辅助功能,不是用来处理运行时错误的机制。使用断言的目的是让开发人员更快速地找到可能导致bug的根本原因。除非程序中存在bug,否则绝不应抛出断言错误。
下面先详细了解一下断言的语法,接着介绍在实际工作中使用断言时常见的两个陷阱。
用断言的使用 的断言语法
在开始使用的某项特性之前,最好先研究它是如何实现的。根据文档,assert
语句的语法如下所示:详见文档:“The Assert Statement”。
assert_stmt ::= "assert" expression1 ["," expression2]
其中expression1
是需要测试的条件,可选的expression2
是错误消息,如果断言失败则显示该消息。在执行时,解释器将每条断言语句大致转换为以下这些语句:
if __debug__:
if not expression1:
raise AssertionError(expression2)
这段代码有两个有趣之处。
第一,代码在检查断言条件之前,还会检查__debug__
全局变量。这是一个内置的布尔标记,在一般情况下为真,若进行代码优化则为假。下一节将进一步讨论。
第二,还可以使用expression2
传递一个可选的错误消息,该消息将与回溯中的AssertionError
一起显示,用来进一步简化调试。例如,我见过这样的代码:
>>> if cond == 'x':
... do_x()
... elif cond == 'y':
... do_y()
... else:
... assert False, (
... 'This should never happen, but it does '
... 'occasionally. We are currently trying to '
... 'figure out why. Email dbader if you '
... 'encounter this in the wild. Thanks!')
虽然这段代码很丑,但如果在应用程序中遇到海森堡bug,那么这绝对是一种有效且有用的技术。
指在尝试研究时似乎会消失或者改变行为的bug,参见维基百科“海森堡bug”词条。
用断言的使用 常见陷阱
在中使用断言时,需要注意两点:第一,断言会给应用程序带来安全风险和bug;第二,容易形成语法怪癖,开发人员会很容易编写出许多无用的断言。
这些问题看上去(而且可能确实)相当严重,所以你应该至少对以下两个注意事项有所了解。
注意事项1:不要使用断言验证数据
在中使用断言时要注意的一个重点是,若在命令行中使用-O
和-OO
标识,或修改C中的OPTIMIZE
环境变量,都会全局禁用断言。
此时所有断言语句都无效,程序会直接略过而不处理断言,因此不会执行任何条件表达式。
许多其他的编程语言也有类似的设计决策。因此使用断言语句来快速验证输入数据非常危险。
进一步解释一下,如果程序使用断言来检查一个函数参数是否包含“错误”或意想不到的值,那么很快就会发现事与愿违并会导致错误或安全漏洞。
下面用一个简单的例子说明这个问题。与前面一样,假设你正在用构建一个在线商店应用程序,代码中有一个函数会根据用户的请求来删除产品。
由于刚刚学习了断言,因此你可能会急于在代码中使用(反正我会这么做)。于是,你写下这样的实现:
def delete_product(prod_id, user):
assert user.is_admin(), 'Must be admin'
assert store.has_product(prod_id), 'Unknown product'
store.get_product(prod_id).delete()
仔细看这个delete_product
函数,如果禁用断言会发生什么?
这个仅有三行代码的函数示例存在两个严重的问题,都是由不正确地使用断言语句引起的。
(1) 使用断言语句检查管理员权限很危险。如果在解释器中禁用断言,这行代码则会变为空操作,不会执行权限检查,之后任何用户都可以删除产品。这可能会引发安全问题,攻击者可能会借此摧毁或严重破坏在线商店中的数据。这太糟糕了!
(2) 禁用断言后会跳过has_product()
检查。这意味着可以使用无效的产品ID调用get_product()
,这可能会导致更严重的bug,具体情况取决于程序的编写方式。在最糟的情况下,有人可能借此对商店发起拒绝服务(denial of service,DoS)攻击。例如,如果尝试删除未知产品会导致商店应用程序崩溃,那么攻击者就可以发送大量无效的删除请求让程序无法工作。
那么如何避免这些问题呢?答案是绝对不要使用断言来验证数据,而是使用常规的if
语句验证,并在必要时触发验证异常,如下所示:
def delete_product(product_id, user):
if not user.is_admin():
raise AuthError('Must be admin to delete')
if not store.has_product(product_id):
raise ValueError('Unknown product id')
store.get_product(product_id).delete()
修改后的示例还有一个好处,即代码不会触发通用的AssertionError
异常,而是触发与语义相关的异常,如ValueError
或AuthError
(后者需要自行定义)。
注意事项2:永不失败的断言
开发人员很容易就会添加许多总是为真的断言,我过去一直犯这样的错误。长话短说,来看看问题所在。
在将一个元组作为assert
语句中的第一个参数传递时,断言条件总为真,因此永远不会失败。
例如,这个断言永远不会失败:
assert(1 == 2, 'This should fail')
这是因为在中非空元组总为真值。如果将元组传递给assert
语句,则会导致断言条件始终为真,因此上述assert
语句毫无用处,永远不会触发异常。
这种不直观的行为很容易导致开发人员写出糟糕的多行断言。比如我曾经欢快地为一个测试套件写了一堆无用的测试用例,带来了并不真实的安全感。假设在单元测试中有这样的断言:
assert (
counter == 10,
'It should have counted all the items'
)
第一次检查时,这个测试用例看起来非常好。但它实际上永远不会得到错误的结果:无论计数器变量的状态如何,断言总是计算为True
。为什么会这样?因为其中只是声明了一个布尔值总是为真的元组对象。
就像之前说的那样,这样很容易就会搬起石头砸自己的脚(我的脚仍然很痛)。有一个很好的对策能防止这种语法巧合导致的麻烦,那就是使用代码linter。新版本的 3也会对这些可疑断言给出语法警告。
顺便说一下,这也是为什么应该总是对单元测试用例先做一个快速的冒烟测试。要确保在编写下一个测试之前,当前测试用例的确会失败。
用断言的使用 总结
尽管有这些需要注意的事项,但的断言依然是功能强大的调试工具,且常常得不到充分的利用。
了解断言的工作方式及使用场景有助于编写更易维护和调试的程序。
学习断言有助于将你的知识提升到新的水平,让你成为一个全方位的高手。我确信这一点,因为断言让我在调试过程中节省了大量时间。
关键要点
- 断言语句是一种测试某个条件的调试辅助功能,可作为程序的内部自检。
- 断言应该只用于帮助开发人员识别bug,它不是用于处理运行时错误的机制。
- 设置解释器可全局禁用断言。