3.3.1 传统的错误检测方法

如何提高程序的健壮性?关键显然在于如何发现运行时错误并加以处理。顾名思义,运行时错误是在程序运行时才暴露的,很难在静态的编译阶段检查出来。传统编程方法中常利 用 if 语句来检测可能导致异常发生的条件,以期发现并处理错误。具体的检测方式有两种, 一种是在执行任务之前检测条件,另一种是执行任务之后检测返回状态码或错误码。

作为例子,我们来编写一个求解一元二次方程的程序。利用初等代数知识,我们知道一 元二次方程 ax2+bx+c=0 的两个根是:

3.3.1 传统的错误检测方法 - 图1

据此很容易写出下面这个程序:

【程序 3.5】eg3_5.py

  1. import math
  2. a, b, c = input("Enter the coefficients (a, b, c): ")
  3. discRoot = math.sqrt(b * b - 4 * a * c)
  4. root1 = (-b + discRoot) / (2 * a)
  5. root2 = (-b - discRoot) / (2 * a)
  6. print "The solutions are:", root1, root2

本程序先由用户输入一元二次方程的三个系数,然后利用公式算出两个根,并显示结果。 这个版本看上去很直接了当,似乎符合预期的功能,但实际上这个版本很有问题。下面我们 来运行这个程序:

  1. >>> import eg3_5
  2. Enter the coefficients (a, b, c): 1,2,3
  3. Traceback (most recent call last):
  4. File "<pyshell#0>", line 1, in <module> import eg3_x
  5. File "eg3_x.py", line 3, in <module> discRoot = math.sqrt(b * b - 4 * a * c)
  6. ValueError: math domain error

由于用户输入的系数 1、2、3 使得一元二次方程的判别式 b2 - 4ac 小于零,因此当程序 运行到调用 math.sqrt 函数时导致错误,程序崩溃并输出上面这一堆错误信息。作为专业的程 序员,对这里发生的一切自然能理解,但作为普通的用户,看到这些天书般的的错误信息时 除了抱怨程序不好用,还能怎么办呢?

为了增强程序 3.5 的健壮性,可以用 if 语句来检查判别式的值,以便区别处理方程有实 数根和无实数根的两种情形,避免在无实数根的情况下崩溃。改进版本如下:

【程序 3.6】eg3_6.py

  1. import math
  2. a, b, c = input("Enter the coefficients (a, b, c): ")
  3. discrim = b * b - 4 * a * c
  4. if discrim &gt;= 0:
  5. discRoot = math.sqrt(discrim)
  6. root1 = (-b + discRoot) / (2 * a)
  7. root2 = (-b - discRoot) / (2 * a)
  8. print "The solutions are:", root1, root2
  9. else:
  10. print "The equation has no real roots!"

从程序中可见,仅当判别式 discrim 大于等于 0 时,才去调用 math.sqrt 函数求其平方根, 这样 sqrt 不会出错,从而避免了程序崩溃;当 discrim 为负数时,并不调用 sqrt,而是向用户 显示一些信息,告诉用户发生了什么,程序同样能正常结束。

下面分别测试程序 3.6 对两种情形的判别式的执行效果:

  1. >>> import eg3_6
  2. Enter the coefficients (a, b, c): 1,2,3
  3. The equation has no real roots!
  4. >>> reload(eg3_6)①
  5. Enter the coefficients (a, b, c): 1,3,2
  6. The solutions are: -1.0 -2.0

从结果可见程序 3.6 确实达到了预期的目的,健壮性得到了增强。

像程序 3.6 这样利用 if 语句来检测可能的出错条件,以阻止可能导致错误的语句的执行, 这是一种常用的错误检测方式。下面介绍另一种错误检测方式。

很多时候要执行的语句实际上是函数调用②,被调用的函数可能是我们自己写的,也可 能是标准函数库里定义的。函数作为一个具有相对独立性的程序块,一般都有自己的错误检 测代码,并根据执行是否正常而返回不同的“错误码”给调用者。这样,函数的调用者可以 无条件地调用函数,然后根据函数返回的错误码来了解函数的执行情况,并基于此来决定下 一步行动。例如,假设有一个求平方根的函数 robustSqrt 在参数为负数时返回错误码-1(由 于实数的平方根总是正数,返回-1 就表明发生了异常):

  1. def robustSqrt(x):
  2. if x < 0:
  3. return -1
  4. else:
  5. return math.sqrt(x)

那我们就可以不必先检测判别式的正负,而是直接调用 robustSqrt,并通过它的返回值来检测 是否发生了异常。示例代码片段如下:

  1. discRoot = robustSqrt(b * b 4 * a * c)
  2. if discRoot < 0:
  3. print "The equation has no real roots!"
  4. else:
  5. root1 = (-b + discRoot) / (2 * a)
  6. root2 = (-b discRoot) / (2 * a)
  7. print "The solutions are:", root1, root2

与程序 3.6 中的错误检测代码相比,上面这种错误检测代码更可取。理由是:函数就像 一个提供特定功能的“黑盒”,我们只需调用其功能,不需了解其内部细节,因此让函数自己 在内部进行错误检测更符合“黑盒”原则。程序 3.6 中的错误检测建立在对函数 math.sqrt 内 部执行细节(即负数导致崩溃)的了解之上,因而不符合“黑盒”原则。

① reload 函数用于重新运行一个已成功导入的模块。

② 关于函数,详见第 4 章。