Python Assert (断言) 笔记
Python Assert 的简单用法,实现原理,使用时的注意点详解。这篇笔记是在我看了 Dan Baber 的《Python Tricks: A Buffet of Awesome Python Features》的第二章时写下的。
Table of Contents
背景
Assert 应该是我很早就看到,但几乎没怎么用过的一个 Python 功能了。这篇笔记是在我看了 Dan Baber 的《Python Tricks: A Buffet of Awesome Python Features》的第二章时写下的。
Assert 的用法
比如我们618或者平时点外卖的时候,平台会有各种满减优惠,折扣优惠券之类的。
我们如果编写一个类似的程序来算最终的价格,肯定要保证一件事情,就是各种折扣算完之后,不能倒给买家送钱,又或者折扣完的价格不能比原价还高。
那么我们可以写一个 apply_discount 的 function ,并在结果返还之前,加一个 assert 语句。
def apply_discount(price, discount):
result = price * (1.0 - discount)
print(result)
assert 0 <= result <= price, "resulting price is unreasonable"
return result
我们可以用一个不合理的价格来测试一下,比如价格是99块,折扣是110%,99 x (1-1.1) = -9.9,也就是倒贴给买家9块9,我们知道这是不合理的。
price = 99
discount = 1.1
apply_discount(price, discount)
接着运行:
$ python assert.py
Assert在这时就会弹出以下的error:
-9.90000000000001
Traceback (most recent call last):
File "d:\Python\assert.py", line 10, in
apply_discount(price, discount)
File "d:\Python\assert.py", line 7, in apply_discount
assert 0 <= result <= price, "resulting price is unreasonable"
AssertionError: resulting price is unreasonable
(注意这里有 Rounding Error,因为计算机采用的是二进制浮点数,但这个问题不在本文的讨论范围内,具体原因可以请各位再搜索如何处理 Rounding Error)
assert 0 <= result <= price, "resulting price is unreasonable"
这一句 Assert 的意思就是,我们要保证 result 在 0 到原价之间,如果不是,要抛出后面定义的 error 语句,也就是我这里写的"resulting price is unreasonable"。
此时我们再回过头来看 Assert 的语法:
assert_stmt ::= "assert" expression1 ["," expression2]
在这里,expression1 就是我们测试的 condition。而 expression2 是optional的,也就是验证为 False 时,我们想要其展示的 error message。
Assert 是如何实现的
在程序运行的时候,Python Interpreter 会将每一句 Assert statement 转换成类似以下的形式:
if __debug__:
if not expression1:
raise AssertionError(expression2)
__debug__ 在这里是一个 global variable,如果我们在运行的时候加上,"-O"或者"-OO"的 command ,会让 __debug__ 的值变为 False,相当于运行时直接忽略Assert。
比如我们再运行一次
$ python -O assert.py
输出就变成了:
-9.90000000000001
会发现这里没有了 Assert Error 的部分。
用 Assert 时的两个注意点
不要用 Assert 来做 Data Validation
比如书中提到的这段代码:
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()
首先这段代码想要实现的功能是删除商店中的一个商品,所以需要检查进行操作的是否为 Admin,以及商店是否的确有这个商品。
这里用 Assert 来检查用户是否为 Admin 是非常危险的,因为正如我们前面提到,在运行 command 加上"-O"可以直接在 Python Interpreter 层面 disable 掉 Assert。那么就会导致这个检查被跳过,也就是说在这里任意用户都可以进行删除商品的行为。
另一个问题在于这里的 has_product 如果被跳过了,那么最后一行调用 get_product() 的时候,就可以使用 invalid 的 product ID,很容易导致更严重的 bug。
解决办法就是绝对不要用 Assert 做 Data Validation,将这里的 Assert 改为 if-else:
def delete_product(prod_id, user):
if not user.is_admin():
raise AuthError("Must be admin to delete")
if not store.has_product(prod_id):
raise ValueError("Unknown product id")
store.get_product(prod_id).delete()
要注意 Assert 的语法,加括号可能会出现问题
你会注意到在我们上面的语法和代码中,Assert 后面是没有跟括号的,因为 Assert 是一个 keyword 但不是一个 function。
Assert 语法:
assert_stmt ::= "assert" expression1 ["," expression2]
所以如果你写出:
assert(1==2, "something is wrong")
这样的代码,对 Python Interpreter 来讲,它会将整个 (1==2, "Something is wrong")
当成 expression1 来处理,而这个 (1==2, "Something is wrong")
对 Python 来讲是一个 tuple,而 Non-Empty Tuple 是 True。
比如在这个例子中就会变成:
if __debug__:
if not assert(1==2, "something is wrong"):
raise AssertionError(expression2)
所以就会导致这个 Assert 是无效的:
assert(1==2, "something is wrong")
不过这里需要注意,如果你写的是:
assert(1==2)
Python是会将其当作 assert 1==2
的,因为只有括号围住的部分只有一个 item,并不会将其当成 tuple,除非你给它加一个逗号。也就是:
assert(1==2,)
那看到这里你可能会想问了,如果我需要写一个跨行的 assert,要怎么办?你可以在每行末尾加个"\":
a = 1
assert a == 2, \
"a should be 2, it is " + str(a) \
+". Please fix it."
输出就会是:
Traceback (most recent call last):
File "d:\Python\assert.py", line 30, in
assert a == 2, \
AssertionError: a should be 2, it is 1. Please fix it.
总结
Assert 应该是作为让开发者快速 debug 的工具而存在,可以帮助我们快速地定位到 bug 的位置,但切记不要用 Assert 来做 data validation。
最后,希望我这篇笔记能对你有所帮助!
参考资料
- Dan Baber的《Python Tricks: A Buffet of Awesome Python Features》的第二章
- Stack Overflow: python assert with and without parenthesis
然的博客 Newsletter
Join the newsletter to receive the latest updates in your inbox.