我们平时在看代码的时候,或多或少会看到过assert的存在,并且在有些code review也可以通过增加assert来使代码更加健壮。但是即便如此,assert还是很容易被人忽略,可是这个很不起眼的用法,如果用的得当的话,会对我们的代码大有裨益。所以,我们今天就来看一看assert的用法。
什么是assert?
Python的assert可以被看做是一个debug的工具,主要测试一个条件是否满足,如果测试的条件满足,则什么也不执行,相当执行了pass语句;而如果条件不符合,则会抛出AssertionError,并返回具体的错误信息(optional)。他的具体语法是这样的
assert_stmt ::='assert' expression [',',Exception]
我们看看一个简单形式的assert expression的例子:
assert 1 == 2
就相当于下面的两行代码:
if __debug__:
if not expression : raise AssertionError
再开看看另外一种格式
assert 1 == 2,'assertion is wrong'
就相当于下面的两行代码格式
if __debut__:
if not expression1:raise AssertionError(expression2)
这里的__debug__是一个常数,如果Python程序执行时候带了-o这个选项,比如
Python -O test.py
那么,程序中所有assert语句都会失效,常数__debug__就为False,反之则为True。
不过要注意的是,直接对常数赋值是非法的,因为他的值在解释器运行的时候已经决定了,中途是无法改变的。
另外还要记住一点:在使用assert的时候一定不要加括号,比如下面的形式
assert (1 == 2,'this should fail')
因为这样写的话,无论表达式是否正确,assert检查都不会是fail,程序只会给出SyntaxWarning的消息,正确的写法是这样的
assert 1 ==2 ,'this should fail'
##########输出##########
AssertionError: this should fail
总的来说,asssert在程序中的作用,是对代码做一些internal的self-check。使用assert,就表示很确定这个条件一定会发生或者一定不会发生。
举个例子,比如这里有一个函数需要给定的参数是人的性别,而正常情况性别之分男和女,我们就可以使用assert来防止程序的非法输入。如果程序没有bug那么assert永远不会抛出异常,而一旦抛出异常就很容易定位程序是在哪里出了问题,便于定位。
assert的用法
讲完了assert的基本语法和概念,我们下面通过一些实际的场景来看看assert在Python中的用法,并弄清楚assert的使用场景。
第一个例子。我们假设超时在做促销活动,准备对一些商品进行打折,那么后台就需要做一个apply_discount()的函数,要求输入为原来的价格和折扣,输出是折扣以后的价格,那么,我们就可以大概写成下面的样子:
def apply_discount(price,discount):
update_price = price*(1-discount)
assert 0 < update_price<price,'price should be greated or equal to 0'
return update_price
然后我们可以通过给定几组数来验证一下他的功能
print(apply_discount(100,0.8))
##########输出##########
19.999999999999996 print(apply_discount(100,2))
##########输出##########
AssertionError: price should be greated or equal to 0
可以看出来,如果discount是0.2的时候,输出是正常的,但是discount如果成了2时,程序就会抛出异常了。这个时候,开发人员修改相关代码或增加进新功能的时候,在运行测试的时候非常容易看出问题。所以,assert的加入,可以有效的预防bug的发生,提高程序的健壮性。
第二个例子,最常见的除法操作是在哪个领域都会遇到的,比方我们想知道一批货物的销售的平均价格,就要给定销售总额还有销售数,这样平均售价就能算出来。
def calculate_average_price(totle_sales,num_sales):
assert num_sales > 0,'number of sales should be greater than 0'
return totle_sales/num_sales
同样我们在函数里增加了assert语句,规定了销售的总数量必须大于0,这样就可以防止后台计算错误。
除了上面两个例子,在实际工作中,assert还有一些很常见的用法,比方下面的场景:
def fun(input):
assert isinstance(input,list),'input must be type of list'
if len(input) == 1:
pass
elif len(input) == 2:
pass
else:
pass
函数里的操作是基于input是个list这个前提,我们函数开始的地方加一个assert检查,防止程序出错。
但是要注意的是,在这个函数中加了assert的前提是我们十分确定程序的输入是一个list,而不能是其他的数据类型。如果我们的这个函数是个多态的,针对不同的数据类型有不同的操作,那就应该写成if...else...的条件语句了
def fun(input):
if isinstance(input,int):
pass
elif isinstance(input,str):
pass
else:
pass
assert的错误示例
通过前面讲的assert的使用场景,有可能会让我们比较迷茫——很多地方都可以使用assert,那么很多if...else的条件语句是不是也可以换成assert呢?这种想法可能就不准确了。接下来,我们看一看几个典型的错误用法:
比方我们要删除一些数据,但是删除操作必须是admin用户才可以,那么就有了下面的代码,
def delete_data(user,data_id):
assert user_is_admin(user),'user must admin'
assert data_exist(data_id),'data must exist'
delete(data_id)
那么上面的代码有什么问题么?
assert的检查是可以被关闭的,在运行Python程序时候,加入一个-O选项就会使assert失效。因此,一旦assert的检查被关闭,user_is_admin()和course_exist()这两个函数就不会执行,就会导致下面的问题:
1.任何用户都可以删除数据;
2.不管数据是否存在,都可以墙纸执行删除操作
这就会给程序带来巨大的安全漏洞,正确的做法,是使用条件语句进行相应的检查,然后抛出相关的异常信息。
def delete_data(user,data_id):
if not user_is_admin(user):
raise Exception('user must be admin')
if not data_exist(data_id):
raise Exception('data must exist')
delete(data_id)
再来看一个例子,我们想打开一个文件,进行数据的处理,读取等一系列操作,那么下面的写法也是有风险的
def read_and_process(path):
assert file_exist(path),'file must exist'
with opne(path) as f:
pass
因为assert的使用,表明强行指定文件必须存在,但事实情况下,这个假设是不成立的,另外,打开文件操作也可能触发别的异常。所以正确的做法是用try...except来解决
def read_and_process(path):
try:
with open(path) as f:
pass
except Exception as e:
pass
总得来说,assert是不实用于run-time error的检查,比方试图打开一个文件,但文件不存在;或者想要从网上down一个文件,但是中途断网了等。这些情况下我们一般使用错误和异常处理。
总结
我们今天学习了assert的用法——assert通常用来对代码进行必要的self-check,表明我们在写代码的时候很确定这种情况一定会发生,或者一定不会发生。需要注意的是,使用assert的时候,一定不能加括号,否则无论表达式对与错,assert的检查永远不会fail。另外,程序中的assert,可以通过-O等选项被全局disable。
通过几个场景的应用,我们可以发现assert的合理使用可以增加代码的健壮度,同时也方便了程序出错时开发人员的定位排查。
不过,我们也要注意assert的使用场合,大多数情况下程序中出现的不同情况都是意料之中的,西药我们用不同的方案来处理,这时候条件语句就更加合适,而程序中的一些run-time error,异常处理就更加合适