什么是 assert?

Python 的 assert 语句,可以说是一个 debug 的好工具,主要用于测试一个条件是否满足,

  • 如果测试的条件满足,则什么也不做,相当于执行了 pass 语句;
  • 如果测试条件不满足,便会抛出异常 AssertionError,并返回具体的错误信息(optional)。

总的来说,assert 在程序中的作用,是对代码做一些 internal 的 self-check。使用 assert,就表示你很确定某个条件一定会发生或者一定不会发生。

举个例子,比如你有一个函数,其中一个参数是人的性别,因为性别只有男女之分(这里只指生理性别),你便可以使用 assert,以防止程序的非法输入。如果你的程序没有 bug,那么 assert 永远不会抛出异常;而它一旦抛出了异常,你就知道程序存在问题了,并且可以根据错误信息,方便定位出错误的源头。

因此,assert 的加入,可以有效预防 bug 的发生,提高程序的健壮性。

具体语法

assert_stmt :=  "assert" expression ["," expression]

# 形式一
assert expression
# 相当于
if __debug__:
    if not expression: raise AssertionError


# 形式二
assert expression1, expression2
# 相当于
if __debug__:
    if not expression1: raise AssertionError(expression2)

这里的__debug__是一个常数。如果 Python 程序执行时附带了-O(字母O)这个选项,比如Python test.py -O,那么常数__debug__为 False,程序中所有的 assert 语句都会失效;反之__debug__则为 True。

不过,需要注意的是,直接在代码中对常数__debug__赋值是非法的,因为它的值在解释器开始运行时就已经决定了,中途无法改变。

注意点 1

一定记住,不要在使用 assert 时加入括号。如果按照这样来写,无论表达式对与错,assert 检查永远不会 fail,程序只会给你 SyntaxWarning。

assert(1 == 2, 'This should fail')
# 输出
<ipython-input-8-2c057bd7fe24>:1: SyntaxWarning: assertion is always true, perhaps remove parentheses?
  assert(1 == 2, 'This should fail')


# 正确写法
assert 1 == 2, 'This should fail'
# 输出
AssertionError: This should fail

注意点 2

assert 的常见用法,是当函数中所有操作,都基于输入必须是某一类型这个前提,那就很有必要在开头加上一句 assert 检查,防止程序出错。

def func(input):
    assert isinstance(input, list), 'input must be type of list'
    # 下面的操作都是基于前提:input必须是list
    if len(input) == 1:
        ...
    elif len(input) == 2:
        ...
    else:
        ...

但如果你的程序中,允许 input 是其他数据类型,并且对不同的数据类型都有不同的处理方式,那就应该写成 if else 的条件语句了:

def func(input):
    if isinstance(input, list):
        ...
    else:
        ...

注意点 3

if 条件语句是不是都可以换成 assert 呢?

答案是否定的。在实际工程中,基本上没人会这么写。原因是 assert 的检查是可以被关闭的,比如在运行 Python 程序时,加入-O这个选项就会让 assert 失效。因此,一旦 assert 的检查被关闭,就显然会给程序带来巨大的安全漏洞。

正确的做法,是使用 if 条件语句进行相应的检查,并合理抛出异常。

因此,不能滥用 assert。很多情况下,程序中出现的不同情况都是意料之中的,需要我们用不同的方案去处理,这时候用条件语句进行判断更为合适。

注意点 4

assert 并不适用 run-time error 的检查。比如试图打开一个文件,但文件不存在;或者是试图从网上下载一个东西,但中途断网了了等等,这些情况下,还是应该通过合理的异常处理。

举个例子,如果你想打开一个文件,进行数据读取、处理等一系列操作。是否能在函数开头加入 assert 检查,判断文件是否存在呢?

def read_and_process(path):
    assert file_exist(path), 'file must exist'
    with open(path) as f:
      ...

答案仍是否定的。因为 assert 的使用,表明你强行指定了文件必须存在,但事实上,在很多情况下,这个假设并不成立。另外,打开文件的操作,也有可能触发其他的异常。

所以,正确的做法是进行异常处理,用 try 和 except 来解决:

def read_and_process(path):
    try:
        with open(path) as f:
            ...
    except Exception as e:
            ...

参考

《Python核心技术与实战》