美文网首页
16 Python异常

16 Python异常

作者: 泷汰泱 | 来源:发表于2019-09-26 10:52 被阅读0次

    什么是异常

    本节开始介绍之前,先看看如下程序:

    >>> print(a)
    Traceback (most recent call last):
      File "<pyshell#1>", line 1, in <module>
        print(a)
    NameError: name 'a' is not defined
    

    是不是很熟悉,这是我们前面经常看到的程序运行出现的错误。
    作为Python初学者,在学习Python编程的过程中,经常会看到一些报错信息,使你编写的程序不能如期工作,如我们前面看到过的NameError、SyntaxError、TypeError、ValueError等,这些都是异常。
    异常是一个事件,该事件会在程序执行过程中发生,影响程序的正常执行。一般情况下,在Python无法正常处理程序时就会发生异常。异常是Python的对象,表示一个错误。当Python脚本发生异常时,我们需要捕获并处理异常,否则程序会终止执行。
    每一个异常都是一些类的实例,这些实例可以被引用,并且可以用很多种方法进行捕捉,使得错误可以被处理,而不是让整个程序失败。

    异常处理

    出现异常怎么办呢?
    就如我们使用的工具出了点小毛病,我们可以想办法修理好它。程序也一样,前辈们经过不断积累与思考,创造了不少好方法处理程序中的异常,最简单的是使用try语句处理。
    try语句的基本形式为try/except。try/except语句用来检测try语句块中的错误,从而让except语句捕获异常信息并处理。如果你不想在发生异常时结束程序,只需在try语句块中捕获异常即可。
    捕获异常的语法如下:

    try:
    <语句> #运行别的代码
    except <名字>:
    <语句> #如果在try部分引发了异常

    ry的工作原理是,开始一个try语句后,Python就在当前程序的上下文中做标记,当出现异常时就可以回到做标记的地方。首先执行try子句,接下来发生什么依赖于执行时是否出现异常。
    如果try后的语句执行时发生异常,程序就跳回try并执行except子句。异常处理完毕,控制流就可以通过整个try语句了(除非在处理异常时又引发新异常)。
    如以下示例所示:

    #!/usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    
    def exp_exception(x,y):
        try:
             a = x/y
             print('a=', a)
             return a
        except Exception:
             print('程序出现异常,异常信息:被除数为0')
    
    exp_exception(2, 0)
    

    程序执行结果如下:

    程序出现异常,异常信息:被除数为0

    由执行结果看到,程序最后执行的是except子句,如果语句正常,应该输出“a=”的形式。
    这里你可能会有疑问:直接在做除法前对y值进行判断不就解决问题了,何必使用try/except语句呢?
    在本例中这么做确实更好一些。如果给程序加入更多除法,就得给每个除法语句加一个判断语句,这样整个代码看上去就是一堆类似if的功能重复判断语句,真正有效的代码没多少。而使用try/except只需要一个错误处理器即可。
    注意:如果没有处理异常,异常就会被“传播”到调用的函数中。如果在调用的函数中依然没有处理,异常就会继续“传播”,直到程序的最顶层。也就是可以处理其他人程序中未处理的异常。

    抛出异常

    Python使用raise语句抛出一个指定异常。我们可以使用类(Exception的子类)或实例参数调用raise语句引发异常。使用类时程序会自动创建实例。
    例如:

    >>> raise Exception
    Traceback (most recent call last):
      File "<pyshell#1>", line 1, in <module>
        raise Exception
    Exception
    >>> raise NameError('This is NameError')
    Traceback (most recent call last):
      File "<pyshell#0>", line 1, in <module>
        raise NameError('This is NameError')
    NameError: This is NameError
    

    由操作结果看到,第一个示例raise Exception引发了一个没有相关错误信息的普通异常,第二个示例输出了一些错误提示。
    如果只想知道是否抛出了异常,并不想处理,使用一个简单的raise语句就可以再次把异常抛出,例如:

    >>> try:
         raise NameError('This is NameError')
    except NameError:
         print('An exception happened!') #后面不加raise
    
    An exception happened!#不加raise,输出对应字符就结束
    
    >>> try:
         raise NameError('This is NameError')
    except NameError:
         print('An exception happened!')
         raise   #最后加一个raise
    An exception happened!
    Traceback (most recent call last):
      File "<pyshell#11>", line 2, in <module>
        raise NameError('This is NameError')
    NameError: This is NameError
    

    由输出结果看到,使用raise可以输出更深层次的异常。在使用过程中,可以借助该方法得到更详尽的异常信息。
    我们前面碰到的NameError、SyntaxError、TypeError、ValueError等异常类称为内建异常类。在Python中,内建的异常类有很多,可以使用dir函数列出异常类的内容,并用在raise语句中,用法如raise NameError这般。表9-1描述了一些重要的内建异常类。


    Python重要的内建异常类

    捕捉多个异常

    我们在前面讲述了处理一个异常的情况,若涉及多个异常,该怎么处理呢?
    Python支持在一个try/except语句中处理多个异常,语法如下:

    try:
    <语句> #运行别的代码
    except <名字1>:
    <语句> #如果在try部分引发了name1 异常
    except <名字2>,<数据>:
    <语句> #如果引发了name2 异常,获得附加数据

    try语句按照如下方式工作:
    首先,执行try子句(在关键字try和关键字except之间的语句)。如果没有发生异常,忽略except子句,try子句执行后结束。如果在执行try子句的过程中发生异常,try子句余下的部分就会被忽略。如果异常的类型和except之后的名称相符,对应的except子句就会被执行。最后执行try语句之后的代码。如果一个异常没有与任何except匹配,这个异常就会传递到上层的try中。一个try语句可能包含多个except子句,分别处理不同的异常,但最多只有一个分支会被执行。
    处理程序将只针对对应try子句中的异常进行处理,而不会处理其他异常语句中的异常,例如:

    #!/usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    def mult_exception(x,y):
        try:
             a = x/y
             b = name
        except ZeroDivisionError:
             print('this is ZeroDivisionError')
        except NameError:
             print('this is NameError')
    
    mult_exception(2,0)
    

    执行结果如下:

    This is ZeroDivisionError

    若把a=1/0注释掉或放到b=name下面,则得到的执行结果为:

    This is NameError

    由执行结果看到,一个try可包含多个except子句,但子句中只有一个分支会被处理。
    当然,你可能会考虑使用if语句,但这样需要考虑是否做了除法运算,做除法运算时是否使用了变量,是否可能有等于0的变量用作被除数等。需要考虑很多种情况,也需要写很多if语句判断,若不经过严密思考和大量测试,很难把所有情况都考虑到。此外,if语句过多会使程序阅读起来比较困难。抛出异常的方式更加简单、直观,可以清晰帮助用户定位问题,并且可以自定义异常信息,进一步定位问题所在。

    使用一个块捕捉多个异常

    上面我们讲述了一个try语句对应多个except子句,若需要一个try对应一个except子句,同时捕捉一个以上异常,可以实现吗?我们先看如下示例:

    #!/usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    def model_exception(x,y):
        try:
             b = name
             a = x/y
        except (ZeroDivisionError, NameError, TypeError):
             print('one of ZeroDivisionError or NameError or TypeError happened')
    
    model_exception(2,0)
    

    程序执行结果如下:

    one of ZeroDivisionError or NameError or TypeError happened

    由执行结果看到,如果需要使用一个块捕捉多个类型异常,可以将它们作为元组列出。使用该方式时,遇到的异常类型是元组中的任意一个,都会走异常流程。
    这么做有什么好处呢?假如我们希望多个except子句输出同样的信息,就没有必要在几个except子句中重复输入语句,放到一个异常块中即可。

    捕捉对象

    如果希望在except子句中访问异常对象本身,也就是看到一个异常对象真正的异常信息,而不是输出自己定义的异常信息,可以使用as e的形式,我们称之为捕捉对象。示例如下:

    #!/usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    def model_exception(x,y):
        try:
             b = name
             a = x/y
        except (ZeroDivisionError, NameError, TypeError) as e:
             print(e)
    
    model_exception(2,0)
    

    执行结果如下:

    name 'name' is not defined

    若a=x/y在前,则结果如下:

    division by zero

    由输出的结果可知,执行过程中抛出的异常被截获并正常输出了相关异常信息,并且使用这种方式可以捕捉多个异常。
    注意:在Python 2中,捕捉对象的子句写法是“Exception,e”,中间使用“,”号分隔,而不是as。此处的e也可以使用其他字母,用e意义比较明确,取自except的首字母。

    全捕捉

    前面我们讲述了很多异常,读者可能以为可以捕捉所有异常,其实并非如此。请看如下示例:

    #!/usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    def model_exception(x,y):
        try:
             b = name
             a = x/y
        except (ZeroDivisionError, NameError, TypeError) as e:
             print(e)
    
    model_exception(2,'')
    

    在该示例中,调用函数时有一个实参传入的是空值。执行结果如下:

    unsupported operand type(s) for /: 'int' and 'str'

    由结果看到,这里抛出的信息并不像我们之前看到的那样,带有明显的Error关键词或异常词。此处只是告知不支持的操作类型。
    在实际编码过程中,即使程序能处理好几种类型的异常,但有一些异常还是会从我们手掌中溜走。上面示例中的异常就逃过了try/except语句的检查,对于这种情况我们根本无法预测会发生什么,也无法提前做任何准备。在这种情况下,与其使用不是捕捉异常的try/except语句隐藏异常,不如让程序立即崩溃。
    如果要处理这种异常,该怎么办呢?先看如下示例:

    #!/usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    def model_exception(x,y):
        try:
             b = name
             a = x/y
        except:
              print('Error happened')
    
    model_exception(2,'')
    

    执行结果如下:

    Error happened

    由程序和执行结果看到,可以在except子句中忽略所有异常类,从而让程序输出自己定义的异常信息。
    当然,这里只给出了一种可参考的解决方式。从实用性方面讲,不建议这么做,因为这样捕捉异常非常危险,会隐藏所有没有预先想到的错误。建议使用抛出异常的方式处理,或者对异常对象e进行一些检查。
    异常中的else
    如果程序执行完异常还需要做其他事情,怎么办呢?
    异常为我们提供了try...except...else语句实现该功能,语法如下:

    try:
    <语句> #运行别的代码
    except <名字>:
    <语句> #如果在try部分引发了异常1
    except <名字>,<数据>:
    <语句> #如果引发了异常2,获得附加数据
    else:
    <语句> #如果没有发生异常

    如果在try子句执行时没有发生异常,就会执行else语句后的语句(如果有else)。使用else子句比把所有语句都放在try子句里面更好,这样可以避免一些意想不到而except又没有捕获的异常。
    例如:

    #!/usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    def model_exception(x,y):
        try:
             a = x/y
        except:
              print('Error happened')
        else:
             print('It went as expected')
    
    model_exception(2,1)
    

    执行结果如下:

    It went as expected

    由执行结果看到,没有发生异常时,会执行else子句的流程。
    综上所述,当程序没有发生异常时,通过添加一个else子句做一些事情(比如输出一些信息)很有用,可以帮助我们更好地判断程序的执行情况。

    自定义异常

    尽管内建异常类包括了大部分异常,而且可满足很多要求,但有时还是要创建自己的异常类。比如需要精确知道问题的根源,就需要使用自定义异常精确定位问题。可以通过创建一个新exception类拥有自己的异常。异常应该继承自Exception类,可以直接继承,也可以间接继承。
    因为错误就是类,捕获一个错误就是捕获该类的一个实例,因此错误并不是凭空产生的,而是由一些不合理的部分导致的。Python的内置
    函数会抛出很多类型的错误,我们自己编写的函数也可以抛出错误。如果要抛出错误,那么可以根据需要定义一个错误的类,选择好继承关系,然后用raise语句抛出一个错误的实例。
    例如:

    #!/usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    class MyError(Exception):
        def __init__(self):
             pass
    
        def __str__(self):
            return 'this is self define error'
    
    def my_error_test():
        try:
             raise MyError()
        except MyError as e:
             print('exception info:', e)
    
    my_error_test()
    

    执行结果如下:

    exception info: this is self define error

    由程序和执行结果看到,程序正确执行了自定义的异常,并且需要继承Exception类。
    这只是一个简单的示例,还有不少细节需要琢磨,此处不做深入探讨,有兴趣的读者可以查阅相关资料进行实践。
    提示:异常最好以Error结尾,一方面贴近标准异常的命名,另一方面便于见名知意。

    finally子句

    Python中的finally子句需要和try子句一起使用,组成try/finally的语句形式,try/finally语句无论发生异常与否都将执行最后的代码。
    例如:

    #!/usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    def use_finally(x,y):
        try:
             a = x/y
        finally:
             print('No matter what happened,I will show in front of you')
    
    use_finally(2,0)
    

    执行结果为:

    No matter what happened,I will show in front of you
    Traceback (most recent call last):
      File "D:/python/workspace/exceptiontest.py", line 65, in <module>
        use_finally(2,0)
      File "D:/python/workspace/exceptiontest.py", line 61, in use_finally
        a = x/y
    ZeroDivisionError: division by zero
    

    由执行结果看到,finally子句被执行了,无论try子句中是否发生异常,finally都会被执行。
    这里我们有一个疑问,虽然执行了finally子句,但是最后还是抛异常了,是否可以使用except截获异常呢?
    可以使用except截获异常。try、except、else和finally可以组合使用,但要记得else在except之后,finally在except和else之后。对于上面的示例,可以更改如下:

    #!/usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    def use_finally(x,y):
        try:
             a = x/y
        except ZeroDivisionError:
             print('Some bad thing happened:division by zero')
        finally:
             print('No matter what happened,I will show in front of you')
    
    use_finally(2,0)
    

    执行结果如下:

    Some bad thing happened:division by zero
    No matter what happened,I will show in front of you

    由执行结果看到,先执行了except子句的输出语句,后面跟着执行了finally子句的输出语句。如果再添加else子句,当程序正常运行时会先执行else子句,然后执行finally子句。在有finally的异常处理程序中,finally中的子句一定是最后执行的。finally子句在关闭文件或数据库连接时非常有用(文件操作和数据库操作后面会具体讲解)。
    提示:在Python 2.5之前的版本中,finally需要独立使用,不能与try语句配合。在Python 2.5之后才支持这些语句的组合使用。

    异常和函数

    异常和函数能够很自然地一起工作。如果异常在函数内引发而不被处理,就会传播至函数调用的地方。如果异常在函数调用的地方也没有被处理,就会继续传播,一直到达主程序。如果在主程序也没有做异常处理,异常就会被Python解释器捕获,输出一个错误信息,然后退出程序。
    例如:

    #!/usr/bin/python3
    # -*- coding:UTF-8 -*-
    def division_fun(x, y):
        return x / int(y)
    
    def exp_fun(x, y):
        return division_fun(x, y) * 10
    
    def main(x,y):
        exp_fun(x, y)
    
    main(2,0)
    

    执行结果如下:

    Traceback (most recent call last):
      File "D:/python/workspace/exceptiontest.py", line 14, in <module>
        main(2,0)
      File "D:/python/workspace/exceptiontest.py", line 12, in main
        exp_fun(x, y)
      File "D:/python/workspace/exceptiontest.py", line 9, in exp_fun
        return division_fun(x, y) * 10
      File "D:/python/workspace/exceptiontest.py", line 6, in division_fun
        return x / int(y)
    ZeroDivisionError: division by zero
    

    由执行结果看到,division_fun函数中产生的异常通过division_funexp_fun函数传播,exp_fun中的异常通过exp_funmain函数传播,传递到函数调用处由解释器处理,最终抛出堆栈的异常信息。
    注意:异常信息是以堆栈的形式被抛出的,因而是从下往上查看的。所谓堆栈,就是最先被发现的异常信息最后被输出(就像子弹入弹夹和出弹夹一样),也被称作先进后出(First In Last Out, FILO)。

    bug的由来

    在编程的过程中,当程序出现问题时,我们就会说出bug了。bug到底是什么意思呢?为什么称之为bug?
    bug一词原本的意思是“臭虫子”或“虫子”,不过现在我们更多将其认为是电脑系统或程序中隐藏的一些未被发现的缺陷或漏洞。
    在20世纪40年代,电子计算机非常庞大,数量也非常少,主要用于军事方面。1944年制造完成的Mark I、1946年2月开始运行的ENIAC和1947年完成的Mark II是赫赫有名的几台计算机。Mark I是由哈佛大学的Howard Aiken教授设计,由IBM公司制造的,Mark II是由美国海军出资制造的。与使用电子管制造的ENIAC不同,Mark I和Mark II主要使用开关和继电器制造。另外,Mark I和Mark II都是从纸带或磁带上读取指令并执行,因此不属于从内存读取和执行指令的存储程序计算机(stored-program computer)。
    1947年9月9日,Mark II计算机在测试时突然发生了故障。经过几个小时的检查,工作人员发现一只飞蛾被打死在面板F的第70号继电器中。把这个飞蛾取出后,机器便恢复了正常。当时为Mark II计算机工作的著名女科学家Grace Hopper将这只飞蛾粘贴在了当天的工作手册中,并在上面加了一行注释First actual case of bug being found,当时的时间是15:45。随着这个故事的广为流传,使用bug一词指代计算机错误的人越来越多,并把Grace Hopper登记的那只飞蛾看作计算机历史上第一个被记录在文档中的bug。

    相关文章

      网友评论

          本文标题:16 Python异常

          本文链接:https://www.haomeiwen.com/subject/pdplyctx.html