返回介绍

3.5 异常处理

发布于 2023-06-02 10:04:35 字数 17149 浏览 0 评论 0 收藏 0

1.恼人的bug

bug一定是程序员最痛恨的生物了。程序员眼中的bug,是指程序缺陷。这些程序缺陷会引发错误或者意想不到的后果。很多时候,程序bug可以事后修复。当然,也存在无法修复的教训。欧洲ARIANE 5火箭第一次发射时,在一分钟之内爆炸。事后调查原因,发现导航程序中的一个浮点数要转换成整数,但由于数值过大溢出。此外,英国直升机于1994年坠毁,29人死亡。调查显示,直升机的软件系统“充满缺陷”。而电影《2001太空漫游》中,超级计算机HAL杀死了几乎所有的宇航员,原因是HAL程序中的两个目标出现了冲突。

在英文中,bug是虫子的意思。工程师很早就开始用bug这个词来指代机械缺陷。而软件开发中使用bug这个词,还有一个小故事。曾经有一只蛾子飞进一台早期计算机,造成这台计算机出错。从那以后,bug就被用于指代程序缺陷。这只蛾子后来被贴在日志本上,至今还在美国国家历史博物馆展出。

很多程序缺陷都可以很早发现并改正。比如,下面程序错用了语法,在for的一行没有加引号。


for i in range(10)
    print(i)

Python不会运行这段程序。它会提醒你有语法错误:


SyntaxError: invalid syntax

下面的程序并没有语法上的错误,但在Python运行时,会发现引用的下标超出了列表元素的范围:


a = [1, 2, 3]
print(a[3])    

程序会中止报错:


IndexError: list index out of range

上面这种只有在运行时,编译器才会发现的错误被称为运行时错误(Runtime Error)。由于Python是动态语言,许多操作必须在运行时才会执行,比如确定变量的类型等。因此,Python要比静态语言更容易产生运行时错误。

还有一种错误,称为语义错误(Semantic Error)。编译器认为你的程序没有问题,可以正常运行。但当检查程序时,却发现程序并非你想做的。通常来说,这种错误最为隐蔽,也最难纠正。比如下面这个程序,目的是打印列表的第一个元素:


bundle = ["a", "b", "c"]
print(bundle[1])

程序并没有错误,正常打印。但你发现,打印出的是第二个元素"b",而不是第一个元素。这是因为Python列表的下标是从0开始的,所以引用第一个元素,下标应该是0而不是1。

2.Debug

修改程序缺陷的过程称为debug。计算机程序具有确定性,所以错误的产生总会有其根源。当然,有时花大量时间都不能debug一段程序,确实会产生强烈的挫败感,甚至认为自己不适合做程序开发。还有的人怒摔键盘,认为电脑在玩自己。就我个人的观察来说,再优秀的程序员,在写程序时也总会产生bug。只不过,优秀的程序员在debug的过程中更心平气和,不会因为bug而质疑自己。他们甚至会把debug的过程当作一种训练,通过更好地理解错误根源来让自己的计算机知识更上一层楼。

其实,debug有点像做侦探。搜集蛛丝马迹的证据,排除清白的嫌疑人,最后留下真凶。收集证据的方法有很多,也有许多现成的工具。对于初学者来说,不需要花太多的时间在这些工具上。在程序内部插入简单的print()函数,就可以查看变量的状态以及运行进度。有时,还可以将某个指令替换成其他形式,看看程序结果有何变化,从而验证自己的假设。当其他可能性都排除了,那么剩下的就是导致错误的真正的原因。

从另一个方面来看,debug也是写程序的一个自然部分。有一种开发程序的方式,就是测试驱动开发(Test-Driven Development,TDD)。对于Python这样一种便捷的动态语言来说,很适合先写一个小型的程序,实现特定的功能。然后,在小程序的基础上,渐进地修改,让程序不断进化,最后满足复杂的需求。整个过程中,你不断增加功能,也不断改正某些错误。重要的是,你一直在动手编程。Python作者本人就很喜欢这种编程方式。因此,debug其实是你写出完美程序的一个必要步骤。

3.异常处理

对于运行时可能产生的错误,我们可以提前在程序中处理。这样做有两个可能的目的:一个是让程序中止前进行更多的操作,比如提供更多的关于错误的信息。另一个则是让程序在犯错后依然能运行下去。

异常处理还可以提高程序的容错性。下面的一段程序就用到了异常处理:


while True:
    inputStr = input("Please input a number:")   # 等待输入
    try:
        num = float(inputStr)
        print("Input number:", num)
        print("result:", 10/num)
    exceptValueError:
        print("Illegal input.Try Again.")
    exceptZeroDivisionError:
        print("Illegal devision by zero.Try Again.")

需要异常处理的程序包裹在try结构中。而except说明了当特定错误发生时,程序应该如何应对。程序中,input()是一个内置函数,用来接收命令行的输入。而float()函数则用于把其他类型的数据转换为浮点数。如果输入的是一个字符串,如"p",则将无法转换成浮点数,并触发ValueError,而相应的except就会运行隶属于它的程序。如果输入的是0,那么除法的分母为0,将触发ZeroDivisionError。这两种错误都由预设的程序处理,所以程序运行不会中止。

如果没有发生异常,比如输入5.0,那么try部分正常运行,except部分被跳过。异常处理完整的语法形式为:


try:
     ...
except exception1:
     ...
except exception2:
    ...
else:
...
finally:
...

如果try中有异常发生时,将执行异常的归属,执行except。异常层层比较,看是否是exception1、exception2……直到找到其归属,执行相应的except中的语句。如果try中没有异常,那么except部分将跳过,执行else中的语句。

finally是无论是否有异常,最后都要做的一些事情。

如果except后面没有任何参数,那么表示所有的exception都交给这段程序处理,比如:


while True:
    inputStr = input("Please input a number:")
    try:
        num = float(inputStr)
        print("Input number:", num)
        print("result:", 10/num)
    except:
        print("Something Wrong.Try Again.")

如果无法将异常交给合适的对象,那么异常将继续向上层抛出,直到被捕捉或者造成主程序报错,比如下面的程序:


def test_func():
    try:
        m = 1/0
    exceptValueError:
        print("Catch ValueError in the sub-function")

try:
    test_func()
exceptZeroDivisionError:
    print("Catch error in the main program")

子程序的try...except...结构无法处理相应的除以0的错误,所以错误被抛给上层的主程序。

使用raise关键字,我们也可以在程序中主动抛出异常。比如:


raiseZeroDivisionError()

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文