master
/ 3.9 异常处理.ipynb

3.9 异常处理.ipynb @masterview markup · raw · history · blame

Notebook

3.8 异常处理

3.8.1 异常

异常是在程序执行过程中发生的一个事件,该事件会影响程序的正常执行。
一般情况下,在Python无法正常处理程序时或者说程序运行时候发生错误而没有被处理时就会发生一个异常。
这些异常会被Python中的内建异常类捕捉, 异常的类型有很多,在前面的学习过程中,遇到过__SyntaxError、 NameError 、TypeError、 ValueError__等多个错误提示信息,这些都是异常。

In [ ]:
# 除数为0时,触发除零异常

a = 8
b = 0
print(a / b)
In [ ]:
# 对象名字拼写错误,变量名未定义异常

pi = 3.14                         # 圆周率
diameter = 4                      # 直径
area = pi * ((dimeter / 2) ** 2)  # 计算圆的面积

当程序发生异常时需要捕获他并进行一些处理,使其__平稳结束__,否则程序会终止执行甚至直接崩溃。
本节主要学习异常的一些处理方法和利用异常进行程序设计。
在程序设计过程中,要尽可能考虑全面,__避免__类似异常的存在,同时,尽可能对可能产生的异常进行__处理__,使程序具有更好的健壮性和容错性,避免程序__崩溃__
也可以利用异常处理的方法实现程序的不同的功能。
Python中有许多内置的异常,有一个内置异常的完整层次结构,每当解释器检测到某类错误时,就能触发相对应的异常。
在程序设计过程中,可以编写特定的代码,专门用于捕捉异常,如果捕捉到某类异常,程序就执行另外一段代码,执行为该异常定制的逻辑,使程序能够正确运行,这种处理方法就是异常处理。

3.8.2 try…except子句

在Python中,可以使用try、except、else和finally这几个关键词来组成一个包容性很好的程序,通过 __捕捉和处理异常__,加强程序的 __健壮性__。用__try__可以检测语句块中的错误,从而让__except__语句捕获异常信息并处理。

try…except语法如下:

In [ ]:
try:
    <语句块1>        #需要检测异常的代码块
except <异常名称1>:
    <语句块2>        #如果在try部份引发了异常名称1时执行的语句块
[except <异常名称2>:
    <语句块3>]       #如果在try部份引发了异常名称2时执行的语句块
[else:
    <语句块4>]       #没有异常发生时执行的语句块
[finally:
    <语句块5>]

except语句和finally语句都不是必须的,但是二者必须要有一个,否则就没有try的意义了。

except语句可以有多个,Python会按except语句的顺序依次匹配指定的异常。

1. 程序首先执行try子句 a. 如果try里面的程序没有触发异常 ⅰ. 跳过except子句 ⅱ. 执行else里面的语句 b. 如果在执行try子句的过程中发生异常 ⅰ. 根据错误类型选择执行对应的except里面的语句,这里面可以是错误信息或者其他的可执行语句。 ⅱ. except可以有多个,分别用于处理不同类型的异常 ⅲ. 每个except只捕捉一个异常,一旦捕捉到异常就不会再进入其他except语句块 ⅳ. except 子句 可以用带圆括号的元组来指定多个异常,except (RuntimeError, TypeError, NameError): ⅴ. 如果发生的异常与 except 子句 中指定的异常不匹配,则它是一个 未处理异常,执行将终止并输出error消息。 ⅵ. except 或 else 子句执行期间也会触发异常,该异常会在 finally 子句执行之后被重新触发 c. 若有finally子句,无论是否发生异常,都执行finally语句块 ⅰ. finally放在最后,其内容通常是做一些后事的处理 ⅱ. 比如关闭文件、资源释放之类的操作 ⅲ. 如果 finally 子句中包含 break、continue 或 return 等语句,异常将不会被重新引发 ⅳ. finally语句块是无论如何都要执行的 ⅴ. 如果执行 try 语句时遇到 break,、continue 或 return 语句,则 finally 子句在执行 break、continue 或 return 语句之前执行。 ⅵ. 如果 finally 子句中包含 return 语句,则返回值来自 finally 子句的某个 return 语句的返回值,而不是来自 try 子句的 return 语句的返回值。
In [8]:
a, b = map(int, input().split())

try:
    import bun
except ZeroDivisionError:
    print('除数为0,不能做除法运算')
除数为0,不能做除法运算

实例4.14 四则运算

随机产生两个整数,输出其加、减、乘、__除__的结果,结果为浮点数时保留小数点后2位。

题目很简单,下面代码可以完成计算,但考虑到随机生成数字时,b的值可能会取到整数0,此时__除数为0__,会触发异常(小概率事件,需多次运行,但理论上一定会出现这个问题。)

In [ ]:
import random


a = random.randint(0, 20)      # 随机产生一个20以内的整数
b = random.randint(0, 3)       # 随机产生一个3以内的整数
sign = random.choice('+-*/')   # 随机产生一个运算符号
print(f"{a} + {b} = {a + b}")
print(f"{a} - {b} = {a - b}")
print(f"{a} * {b} = {a * b}")
print(f"{a} / {b} = {a / b:.2f}")

注意到0不能做除数,所以当第二个数字为0时,程序要能够对异常输入进行处理,使程序不至于因除零异常而崩溃。

可以将__可能会触发异常的语句__“print(f"{a} / {b} = {a / b:.2f}")”放到__try__的语句块中,一旦触发异常,程序将执行__except__下面的语句块中的语句。

In [ ]:
import random


a = random.randint(0, 20)      # 随机产生一个20以内的整数
b = random.randint(0, 3)       # 随机产生一个3以内的整数
sign = random.choice('+-*/')   # 随机产生一个运算符号
print(f"{a} + {b} = {a + b}")
print(f"{a} - {b} = {a - b}")
print(f"{a} * {b} = {a * b}")
try:
    print(f"{a} / {b} = {a / b:.2f}")
except ZeroDivisionError:
    print('除数为0,不能做除法运算')

此题中非常肯定的知道只有当产生的 “b”值为“0”时,才会陷入异常,这种__问题明确__的情况下,尽可能用__if…else__ 语句进行处理。

In [ ]:
import random


a = random.randint(0, 20)      # 随机产生一个20以内的整数
b = random.randint(0, 5)       # 随机产生一个5以内的整数
sign = random.choice('+-*/')   # 随机产生一个运算符号
print(f"{a} + {b} = {a + b}")
print(f"{a} - {b} = {a - b}")
print(f"{a} * {b} = {a * b}")
if b == 0:
    print('除数为0,不能做除法运算')
else:
    print(f"{a} / {b} = {a / b:.2f}")

Python允许在一个程序里同时对多类异常进行捕捉,触发哪个异常就执行哪个异常对应的语句。
表3.6列出了Python中常见的异常名称及其描述,可以参考Python文档查看所有异常类及其子类。

异常名称 描述
Exception 常规异常的基类,可以捕获任意异常
Syntax__Error__ 语法错误
Name__Error__ 未声明/未初始化的对象(没有属性)
System__Error__ 一般的解释器系统错误
Value__Error__ 传入无效的参数,或传入一个调用者不期望的值,即使值的类型是正确的
Indentation__Error__ 缩进错误(代码没有正确对齐)
Import__Error__ 导入模块/对象失败(路径问题或名称错误)
ModuleNotFound__Error__ 模块不存在
ZeroDivision__Error__ 除(或取模)
Overflow__Error__ 数字运算超出最大限制
Attribute__Error__ 对象没有这个属性
Index__Error__ 索引超出序列边界,如x只有10个元素,序号为0-9,程序中却试图访问x[10]
Key__Error__ 映射中没有这个键(试图访问字典里不存在的键)
Type__Error__ 对类型无效的操作
Tab__Error__ Tab和空格混用
Runtime__Error__ 一般的运行时错误
In [ ]:
try:
    import turtle             # import tutle  时输出“模块名称有误”
    size = eval(input())
    print(size)               # 参数写成sizee时会输出“变量未定义”
    turtle.circle(size)
    turtle.done()             # done写成one时,会输出“属性不存在”
except ModuleNotFoundError:   # 遇到不同类型的异常给出不同的错误提示
    print('模块名称有误')
except NameError:
    print('变量未定义')
except AttributeError:
    print('属性不存在')
except SyntaxError:
    print('存在语法错误')

Python内置了的“__Exception__”类可以捕捉到所有内置的、非系统退出的异常,以及所有用户定义的异常。当需要输出程序遇到的异常时,可以使用以下方法:

In [ ]:
try:
    import tutle  # 修改为import tutle  后运行时输出No module named 'tutle'
 
    size = eval(input())
    print(size)    # size写成sizee时输出name 'sizee' is not defined
    turtle.circle(size)
    # 写成circe时输出 module 'turtle' has no attribute 'circe'
    turtle.done()
except Exception as e:
    print(e)

3.8.3 else子句

没有触发异常时执行的语句

3.8.4 finally子句

如果try中的异常没有在“Exception”中被指出,那么系统将会抛出默认错误代码(Traceback),并且终止程序,接下来的所有代码都不会被执行。
但如果有__finally关键字__存在,则会在程序抛出默认错误代码之前,__执行finally中的语句__
这个方法在某些必须要结束的操作中颇为有用,如__释放文件句柄__,或__释放内存空间__等。

In [ ]:
s = '''  静夜思
   李白
床前明月光,
疑是地上霜。
举头望明月,
低头思故乡。
    '''
try:
    file = open('test.txt', 'w', encoding='utf-8')   # 以“写”模式打开文件
    file.write(s)                                    # 写入s中的字符串
    file.seek(0)                                     # 文件指针回到文件开头
    for line in file:                                # 遍历逐行读文件
        print(line, end='')                          # 逐行输出文件内容
    # file.close()如果放在此处,如果前面遇到异常,将无法关闭文件
except:
    print('文件读写权限错误')
finally:
    file.close()  # finally中的语句无论是否触发异常都会被执行,可确保文件关闭

3.8.5 异常处理的应用

在一些特殊情况下,可以应用异常来实现一些特定的功能。例如正整数A+B的问题,利用其他语言实现可能需要近100行代码,而用Python结合异常处理来实现,仅需不到20行就可以实现。

实例 4.15 正整数A+B

题的目标很简单,就是求两个正整数A和B的和,其中A和B都大于0。稍微有点麻烦的是,输入并不保证是两个正整数。
输入在一行给出A和B,其间以空格分开。
问题是A和B不一定是满足要求的正整数,有时候可能是超出范围的数字、负数、带小数点的实数、甚至是一堆乱码。‪‪‪‪‪‪‪‪‪‪‪‪‪‪‪‪‪‪‫‪‪‪‪‪‪‪‪
我们把输入中出现的第1个空格认为是A和B的分隔。题目保证至少存在一个空格,并且B不是一个空字符串。‪‪‪‪‪‪‪‪‪‪‪‪‪‪‪‪‪‪‫‪‪‪‪‪‪‪‪
如果输入的确是两个正整数,则按格式“A + B = 和”输出。如果某个输入不合要求,则在相应位置输出“?”,显然此时和也是“?”。

In [ ]:
a, b = input().split(' ', maxsplit=1)  # 根据空格切分输入为列表,切分一次
try:
    a = int(a)  # 列表第一个元素转整型,转换类型不成功则触发异常
    if a <= 0:  # 如转换成功,说明是整数,再判定是否为负数
        a = '?'
except ValueError:
    a = '?'     # 当异常被触发时,表明不是整数,用?代替
try:
    b = int(b)  # 列表第二个元素转整型,转换类型不成功则触发异常
    if b <= 0:  # 如转换成功,说明是整数,再判定是否为负数
        b = '?'
except ValueError:
    b = '?'     # 当异常被触发时,表明不是整数,用?代替
if a == '?' or b == '?':
    print(f'{a} + {b} = {"?"}')
else:
    print(f'{a} + {b} = {a + b}')

虽然try...except可以捕捉和处理程序中的异常,但不能过于依赖这种方法。在程序设计过程中,首先应该尽可能排除语法错误与逻辑错误,防御性方式编码比捕捉异常方式更好,应尽量采取这种编程方式,提升性能并且使程序更健壮。
不要试图用try语句解决所有问题,这将会极大的降低程序的性能。只有在错误发生的条件无法预知的情况下,才使用try...except进行处理。
在程序设计过程中,一般情况下异常处理与程序主要的功能是没有关系的,过多的应用异常处理,会导致代码可读性变差。要尽量减少try/except语句块中的代码量,try语句块的体积越大, 期望之外的异常就越容易被触发,越容易隐藏真正的错误,从而带来严重后果。
使用finally子句来执行那些无论try语句块中有没有异常都应该被执行的代码,常用于终止处理程序,这对于清理资源常常很有用,例如关闭文件。

In [ ]: