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。

最后,希望我这篇笔记能对你有所帮助!

参考资料

PythonPython Tricks

Comments