错误和异常

编写计算机程序时,通常能够区分正常和异常(不正常)情况。异常事件可能是错误(如试图除以零),也可能是通常不会发生的事情。为处理这些异常事件,可在每个可能发生这些事件的地方都使用条件语句。例如,对于每个除法运算,都检查除数是否为零。然而,这样做不仅效率低下、缺乏灵活性,还可能导致程序难以卒读。你可能很想忽略这些异常事件,希望它们不会发生,但Python提供功能强大的替代解决方案——异常处理机制。

一、错误分类

程序中难免出现错误,而错误类型分为两种

1.1 语法错误

这种错误,根本过不了python解释器的语法检测,必须在程序执行前就改正

  1. #语法错误示范一
  2. if
  3. #语法错误示范二
  4. def test:
  5. pass
  6. #语法错误示范三
  7. print(haha
  8. #语法错误

1.2 逻辑错误

  1. #用户输入不完整(比如输入为空)或者输入非法(输入不是数字)
  2. num=input(">>: ")
  3. int(num)
  4. #无法完成计算
  5. res1=1/0
  6. res2=1+'str'
  7. # 没有此键值对
  8. dic = {"key": 222}
  9. dic[1]
  10. # 超出索引范围
  11. l1 = [1, 2, 3]
  12. l1[100]

异常就是程序运行时发生错误的信号,在python中,错误触发的异常如下

13. 错误和异常 - 图1

二、异常

Python使用异常对象来表示异常状态,并在遇到错误时引发异常。异常对象未被处理(或捕 获)时,程序将终止并显示一条错误消息(traceback)。

  1. >>> 1 / 0
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in ?
  4. ZeroDivisionError: integer division or modulo by zero

如果异常只能用来显示错误消息,就没多大意思了。但事实上,每个异常都是某个类(这里是ZeroDivisionError)的实例。你能以各种方式引发和捕获这些实例,从而逮住错误并采取措施,而不是放任整个程序失败。

2.1 常见异常

除了ZeroDivisionError异常外,Python中还有很多异常,下面是一些常见的错误:

  1. AttributeError #试图访问一个类中不存在的成员(包括:成员变量、属性和成员方法)而引发的异常
  2. IOError #输入/输出异常;基本上是无法打开文件
  3. ImportError #无法导入模块或包;基本上是路径问题或名称错误
  4. IndentationError #语法错误(的子类);代码没有正确对齐
  5. IndexError #下标索引超出序列边界,比如当x只有三个元素,却试图访问x[5]
  6. KeyError #试图访问字典里不存在的key
  7. KeyboardInterrupt #Ctrl+C被按下
  8. NameError #使用一个还未被赋予对象的变量
  9. SyntaxError Python #代码非法,代码不能编译(个人认为这是语法错误,写错了)
  10. TypeError #传入对象类型与要求的不符合
  11. UnboundLocalError # 试图访问一个还未被设置的局部变量,基本上是由于另有一个同名的全局变量,
  12. #导致你以为正在访问它
  13. ValueError #传入一个调用者不期望的值,即使值的类型是正确的
  14. MemoryError #内存错误,内存不足

2.2 什么是异常处理

  • python解释器检测到错误,触发异常(也允许程序员自己触发异常)
  • 程序员编写特定的代码,专门用来捕捉这个异常(这段代码与程序逻辑无关,与异常处理有关)
  • 如果捕捉成功则进入另外一个处理分支,执行你为其定制的逻辑,使程序不会崩溃,这就是异常处理

2.3 为什么要有异常处理

python解析器去执行程序,检测到了一个错误时,触发异常,异常触发后且没被处理的情况下,程序就在当前异常处终止,后面的代码不会运行,谁会去用一个运行着突然就崩溃的软件。所以你必须提供一种异常处理机制来增强你程序的健壮性与容错性 。

2.4 常用的异常处理的方式

其实我们之前已经使用过一种异常处理的方式了,那就是条件判断语句if

  1. num1=input('>>: ') #输入一个字符串试试
  2. int(num1)
  3. num1=input('>>: ') #输入一个字符串试试
  4. if num1.isdigit():
  5. int(num1) #我们的正统程序放到了这里,其余的都属于异常处理范畴
  6. elif num1.isspace():
  7. print('输入的是空格,就执行我这里的逻辑')
  8. elif len(num1) == 0:
  9. print('输入的是空,就执行我这里的逻辑')
  10. else:
  11. print('其他情情况,执行我这里的逻辑')
  12. '''
  13. 问题一:
  14. 使用if的方式我们只为第一段代码加上了异常处理,但这些if,跟你的代码逻辑并无关系,这样你的代码会因为可读性差而不容易被看懂
  15. 问题二:
  16. 这只是我们代码中的一个小逻辑,如果类似的逻辑多,那么每一次都需要判断这些内容,就会倒置我们的代码特别冗长。
  17. '''
  18. #使用if判断进行异常处理

总结:

  1. if判断式的异常处理只能针对某一段代码,对于不同的代码段的相同类型的错误你需要写重复的if来进行处理。
  2. 在你的程序中频繁的写与程序本身无关,与异常处理有关的if,会使得你的代码可读性极其的差
  3. if是可以解决异常的,只是存在以上的问题,所以,千万不要妄下定论if不能用来异常处理。

你之前使用的异常处理机制:

  1. def test():
  2. print('test running')
  3. choice_dic={
  4. '1':test
  5. }
  6. while True:
  7. choice=input('>>: ').strip()
  8. if not choice or choice not in choice_dic:continue #这便是一种异常处理机制啊
  9. choice_dic[choice]()
  10. #你之前用的异常处理机制

2.4.1 基本语法

  1. try:
  2. 被检测的代码块
  3. except 异常类型:
  4. try中一旦检测到异常,就执行这个位置的逻辑

2.4.2 异常类只能用来处理指定异常情况

  1. # 未捕获到异常,程序直接报错
  2. s1 = 'hello'
  3. try:
  4. int(s1)
  5. except IndexError as e: #通过别名输出异常的具体内容
  6. print e

2.4.3 单分支

只能处理单个异常,只要检测出ValueError的错误,不让程序中断,立马跳转到except语句,实现程序分流的效果。

  1. try:
  2. int(input('>>>'))
  3. except ValueError:
  4. print('必须输入数字。。。')
  5. print(111)

2.4.4 多分支

代码尝试运行try下面的代码,从上至下依次运行,只要出现了逻辑错误即异常,马上跳转到except语句,从上至下依次匹配错误类型,匹配成功,程序跳转,否则报错。

  1. s1 = 'hello'
  2. try:
  3. int(s1)
  4. dic = {"name": 'taibai'}
  5. dic[1]
  6. l1 = [1, 2, 3]
  7. l1[100]
  8. except IndexError as e:
  9. print(e)
  10. except KeyError as e:
  11. print(e)
  12. except ValueError as e:
  13. print(e)

2.4.5 万能异常

可以捕获python给你提供的所有异常错误类型。

  1. s1 = 'hello'
  2. try:
  3. int(s1)
  4. except Exception as e:
  5. print(e)

你可能会说既然有万能异常,那么我直接用上面的这种形式就好了,其他异常可以忽略

你说的没错,但是应该分两种情况去看

  1. 如果你想要的效果是,无论出现什么异常,我们统一丢弃,或者使用同一段代码逻辑去处理他们,那么骚年,大胆的去做吧,只有一个Exception就足够了。
  1. s1 = 'hello'
  2. try:
  3. int(s1)
  4. except Exception,e:
  5. '丢弃或者执行其他逻辑'
  6. print(e)
  7. #如果你统一用Exception,没错,是可以捕捉所有异常,但意味着你在处理所有异常时都使用同一个逻辑去处理(这里说的逻辑即当前expect下面跟的代码块)
  1. 如果你想要的效果是,对于不同的异常我们需要定制不同的处理逻辑,那就需要用到多分支了。
  1. s1 = 'hello'
  2. try:
  3. int(s1)
  4. except IndexError as e:
  5. print(e)
  6. except KeyError as e:
  7. print(e)
  8. except ValueError as e:
  9. print(e)
  10. #多分支

2.4.6 多分支+万能异常

还有一种是多分支+万能异常的处理情况,发生的异常中,有一些异常是需要不同的逻辑处理的,剩下的异常统一处理掉即可,无需进一步判断或者研究,这样的情况下,这种异常处理是最好的方式。

实战项目应用举例:

  1. dic = {
  2. 1: login,
  3. 2: register,
  4. 3: dariy,
  5. 4: article,
  6. 5: comment,
  7. }
  8. print('''
  9. 欢迎访问博客园系统:
  10. 1,登录
  11. 2,注册
  12. 3,访问日记页面
  13. 4,访问文章页面
  14. 5,访问评论页面
  15. ''')
  16. try:
  17. choice = int(input('请输入:'))
  18. dic[choice]()
  19. # if choice.isdigit():
  20. # if
  21. #
  22. # else:
  23. # print('请输入数字...')
  24. except ValueError:
  25. print('请输入数字....')
  26. except KeyError:
  27. print('您输入的选项超出范围...')
  28. except Exception as e:
  29. print(e)

2.4.7 异常处理其他功能

异常处理除了常用了try和except组合之外,还有一些其他的组合,我们先整体看一下:

  1. s1 = 'hello'
  2. try:
  3. int(s1)
  4. except IndexError as e:
  5. print(e)
  6. except KeyError as e:
  7. print(e)
  8. except ValueError as e:
  9. print(e)
  10. #except Exception as e:
  11. # print(e)
  12. else:
  13. print('try内代码块没有异常则执行我')
  14. finally:
  15. print('无论异常与否,都会执行该模块,通常是进行清理工作')
  • try…except…else组合

与循环中的else比较类似,try代码中,只要出现了异常,则不会执行else语句,如果不出现异常,则执行else语句。

比如我们完成一个转账功能的代码,需要一个转账给另一个人,然后另一个人确认收到才算是转账成功,我们用伪代码写一下,他就可以用在这个地方:

  1. # 伪代码
  2. try:
  3. print('扣第一个人钱')
  4. ...
  5. print('给第二个人加钱')
  6. except ValueError:
  7. print('必须输入数字。。。')
  8. else:
  9. print('转账成功')
  • try…excet…finally组合

finally这个用法比较有意思,他是在捕获异常发生之前,先执行finally的代码,有点未卜先知的意思。

  • 如果出现异常并且成功捕获了,finally会在try中最后执行。
    ``` try: dic = {‘name’: ‘太白金星’} print(dic[1])

except KeyError: print(‘出现了keyError错误….’)

finally: print(‘正常执行’)

  1. - 如果出现异常但是没有成功捕获,finally会在异常发生之前执行。

try: dic = {‘name’: ‘太白金星’} print(dic[1])

except NameError: print(‘出现了NameError错误….’)

finally: print(‘异常发生之前,先执行我’)

  1. - finally用在哪里呢?
  2. - 关闭文件的链接链接,数据等链接时,需要用到finally

f = open(‘file’,encoding=’utf-8’) try: ‘’’各种操作’’’ print(f.read()) ‘’’但是发生错误了, 此时没关闭文件句柄,所以’’’

finally: f.close()

  1. - 函数中,finally也会在return之前先执行。

def func(): try: return 1 finally: print(‘finally’) func()

  1. - 循环中,finally也会在return之前执行。

while 1: try: break finally: print(‘finally’)

  1. finally一般就是收尾工作,在一些重要环节出错之前必须一定要做的比如关闭链接的问题时,最好是用上finally作为最后一道防线,收尾。
  2. <a name="a397a8c0"></a>
  3. ### 2.5 主动出发异常
  4. 主动发出异常

raise TypeError(‘类型错误’)

  1. <a name="40071d97"></a>
  2. ### 2.6 断言
  3. 表示一种强硬的态度,只要assert后面的代码不成立,直接报错,下面的代码就不让你执行。

assert 条件

assert 1 == 1

assert 1 == 2

应用:

assert 条件 代码 代码 …….


<a name="5a411432"></a>
### 2.7 自定义异常

python中给你提供的一些错误类型并不是所有的,只是常见的异常,如果以后你在工作中,出现了某种异常无法用已知的错误类型捕获(万能异常只能捕获python中存在的异常),那么你就可以尝试自定义异常,只要继承BaseException类即可。

class EvaException(BaseException): def init(self,msg): self.msg=msg def str(self): return self.msg

try: raise EvaException(‘类型错误’) except EvaException as e: print(e)


<a name="65e6becf"></a>
## 三、异常处理正确的使用方式

有的同学会这么想,学完了异常处理后,好强大,我要为我的每一段程序都加上try…except,干毛线去思考它会不会有逻辑错误啊,这样就很好啊,多省脑细胞===》2B青年欢乐多

try…except应该尽量少用,因为它本身就是你附加给你的程序的一种异常处理的逻辑,与你的主要的工作是没有关系的,这种东西加的多了,会导致你的代码可读性变差,只有在有些异常无法预知的情况下,才应该加上try…except,其他的逻辑错误应该尽量修正。

<a name="1d4dc985"></a>
## 四、调试

我们写的程序能一次成功执行的概念是很低的不足1%,程序中可能有各种错误和异常,有些错误和异常根据异常信息就可以容易的解决,有些则非常复杂,需要进行各种调试。常见的调试方法有以下几种:

<a name="2609bf61"></a>
### 4.1 print语句

在程序可能出错的地方打印各种变量等信息,进行判断。这种方法最原始,效率很低。

def foo(s): n = int(s) print(‘>>> n = %d’ % n) return 10 / n

def main(): foo(‘0’)

main()


<a name="445e4bc2"></a>
### 4.2 断言 assert

所有使用print进行调试输出的地方都可以使用断言,assert后面跟有判断条件,如果条件成立,则进行正常运算逻辑,否则抛出AssertionError异常。

def foo(s): n = int(s) assert n != 0, ‘n is zero!’ return 10 / n

def main(): foo(‘0’) main()


程序中如果到处充斥着assert,和print()相比也好不到哪去。不过,启动Python解释器时可以用-O参数来关闭assert:

python -O abc.py


<a name="4514708e"></a>
### 4.3 logging

把print()替换为logging是第3种方式,和assert比,logging不会抛出错误,而且可以输出到文件:

‘’’ 日志级别: debug < info < warning < error < critical logging.debug(‘debug级别,最低级别,一般开发人员用来打印一些调试信息’) logging.info(‘info级别,正常输出信息,一般用来打印一些正常的操作’) logging.warning(‘warning级别,一般用来打印警信息’) logging.error(‘error级别,一般用来打印一些错误信息’) logging.critical(‘critical 级别,一般用来打印一些致命的错误信息,等级最高’) ‘’’ import logging

logging.basicConfig( level = logging.INFO, #设置日志级别,默认warning format = ‘%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s’, datefmt = ‘%a,%d %b %Y %H:%M:%S’, filename = ‘test.log’, #可以不输出到文件,关闭就输出到终端 filemode = ‘w’ ##模式,有w和a,w就是写模式,每次都会重新写日志,覆盖之前的日志

#a是追加模式,默认如果不写的话,就是追加模式

)

def foo(s): n = int(s) logging.info(‘n = %d’ %n) return 10 / n

def main(): foo(‘0’)

main()


<a name="85eb5606"></a>
### 4.4 Python调试器 pdb

比如我们有个python文件test.py

def div(num1,num2): r = num1 / num2 print(‘result: {}’.format(r))

div(6,0)


我们可以 python -m pdb test.py 的方式来单步执行程序。

1. 执行 python -m pdb test.py 进入到 pdb环境 (Pdb)
1. l(小写字母L) :是显示脚本并且指示(->)出下一步执行的某行代码。
1. n : 执行下一条代码 也就是 -> 指向的代码,并且返回再次执行的代码内容。
1. p <变量名> : 打印当前变量内容
1. q : 退出 pdb 环境
1. help :帮助

![](https://s2.ax1x.com/2019/12/16/Q53V4H.png#crop=0&crop=0&crop=1&crop=1&id=oD8DT&originHeight=330&originWidth=810&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)

上面的方法理论上是万能的,不过过于麻烦,实践性不强,因为有时代码是特别长的,比如1000行,如果执行到最后至少要按1000个 n 。其实我们没有必要所有的地方都单步执行,只需要在我们认为可以出错的地方暂停跟踪即可。

比如我们有个Python文件test.py

import pdb

num1 = 6 num2 = 0 pdb.set_trace() #设置断点 ret = num1 / num2 print(‘{} / {} = {}’.format(num1, num2, ret))


1. 执行python test.py程序
1. 小写字母l进行查看 代码刚好执行完pdb.set_trace()我们设置的跟踪点位置
1. 尝试查看感兴趣的变量
1. c :继续执行脚本,因为我们的脚本是有异常的,会正常抛出异常

![](https://s2.ax1x.com/2019/12/16/Q58QzR.png#crop=0&crop=0&crop=1&crop=1&id=TdfMu&originHeight=362&originWidth=585&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)

<a name="bc00b857"></a>
### 4.5 ipython

当你在有问题的代码前加上这段代码,它就可以帮助你在异常时进入ipdb调试模式。前提是你要安装ipython。

import sys from IPython.core import ultratb sys.excepthook = ultratb.FormattedTB(mode=’Verbose’,color_scheme=’Linux’,call_pdb=1)


你也可以把上面的代码保存成crash_debug.py,然后在你出问题的文件test.py头上加上。

import crash_debug for i in range(10,-1,-1): y = 1/i print(y) ```

执行python3 test.py后输出:
13. 错误和异常 - 图2