一、概述
- 在程序运行中,总会遇到各种各样的错误,有的错误是在程序编写时有问题造成的,例如:
本来应该输出整数却输出了字符串
这种错误通常称之为
BUG
,BUG是必须要修复的
- 除了在编写时导致的错误,还有的错误是在用户输入是造成的,例如:
程序让用户输入一个邮箱地址,而用户输入了一串空字符串
这样的错误我们可以通过检查用户输入的数据来避免
- 还有一种错误,是完全无法在程序运行过程中预测的,例如:
写入磁盘的时候,磁盘空间满了,从网络抓取数据时,网络断开了
这种错误也叫做异常,在程序中通常是必须要做处理的,否则,程序会因为各种问题中止退出
而在Python中,Python内置了一套异常处理机制,帮助我们进行错误处理,除此之外,我们也需要跟踪程序的执行,查看变量的值是否正确,而这个过程就叫做调试,Python中的pdb可以让我们以单步方式执行代码
最后,编写测试也很重要,有了良好的测试,就可以在程序修改后反复运行,确保程序输出符合我们的要求
二、错误处理
在程序运行过程中,如果发生了错误,可以实现返回一个错误代码,这样就可以知道是否存在错误以及出现错误的原因
在操作系统提供的调用中,返回错误代码是非常常见的,例如函数open(),这个函数可以打开一个文件,成功会返回一个文件描述符即一个整数,失败则会返回-1
但是使用上述的错误码来表示代码是否出现错误是非常不方便的,这是因为函数本身返回的正常结果会和错误码混在一起,这样的结果就是我们需要写大量的代码区判断是否出现错误,例如:
- 创建一个'test_1'函数,模仿传入参数不正确时,返回不一样的数据,然后使用'test_2'函数进行判断 # -*- coding: utf-8 -*- def test_1(x): if x > 1: return 1 else: return (-1) def test_2(): a = test_1(2) if a == 1: return 'ok' else: return 'error' print(test_2()) #输出: ok - 更换一下参数 # -*- coding: utf-8 -*- def test_1(x): if x > 1: return 1 else: return (-1) def test_2(): a = test_1(0) if a == 1: return 'ok' else: return 'error' print(test_2()) #输出: error
- 上面只是模拟了一下,而高级语言通常都内置了一套
try...except...finally...
的错误处理机制
,Python当然也有
- try
- 下面来看一下关于
try
的案例,从而了解try
的机制:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- try: print('try...') r = 10 / 0 #10/0会产生一个除法运算错误 print('result:', r) except ZeroDivisionError as e: #这里的 ZeroDivisionError是报错类型 print('except:', e) finally: print('finally...') print('END') - 当我们认为某些代码可能会出错时,可以使用'try'来运行这段代码,如果执行的确出错了,那么后续的代码将不会执行,而是直接跳转到错误处理代码,即'except'语句块,在执行完'except'之后,如果还有'finally'语句块的话,那么就执行'finally'语句块的代码,至此执行完成 - 需要注意的是,如果'try'语句块没有发生错误,那么下一步会跳转至'finally'语句块 - 所以当try发生错误时的执行顺序是'try——>except——>finally' - 当try没有发生错误的执行顺序是'try——>finally' - 10 / 0 注释: ———————————————— >>> 10/0 Traceback (most recent call last): File "<stdin>", line 1, in <module> ZeroDivisionError: division by zero #可以发现这个报错信息和输出的报错信息是相同的 ———————————————— #输出: try... except: division by zero finally... END - 从输出可以看到,在错误发生时,'r = 10 / 0 '的后面'print('result:', r)'并没有执行,而是去了'except'语句块执行了'print('except:', e)',然后到'finally'语句块执行了'print('finally...')',最后执行了'print('END')',因为'print('END')'并不包含在'try'语句块中
然后我们修改一下代码,使r
变量可以成功赋值:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- try: print('try...') r = 10 / 2 print('result:', r) except ZeroDivisionError as e: print('except:', e) finally: print('finally...') print('END') #输出: try... result: 5.0 finally... END - 可以看到当'try'语句块没有发生错误时,执行'try'语句块后,会跳转到'finally'语句块 - 注意,'finally'语句块可以不加
看过上面的案例后,有一步except ZeroDivisionError as e:,我们发现了10 / 0 的错误类型和except ZeroDivisionError as e:这里的错误类型是一样的,其实错误类型还有很多种,这里只是捕获了一种错误类型
而想要处理不同的错误类型,就需要创建不同的except语句块来捕获,例如:
- 修改代码,可以添加多个错误捕获,例如'ValueError' #!/usr/bin/env python3 # -*- coding: utf-8 -*- try: print('try>>>>') r = 10 / int('aaa') print('result: %s' % r) except ValueError as a: print('ValueError: %s' % a) except ZeroDivisionError as b: print('ZeroDivisionError: %s' % b) finally: print('finally>>>>') print('END>>>>') #输出: try>>>> ValueError: invalid literal for int() with base 10: 'aaa' finally>>>> END>>>> - 可以发现写了两个'except'语句块,最后只输出了'ValueError'语句块的内容,这是因为在'int('aaa')'的时候就报错了,修改一下 #!/usr/bin/env python3 # -*- coding: utf-8 -*- try: print('try>>>>') r = 10 / int('0') print('result: %s' % r) except ValueError as a: print('ValueError: %s' % a) except ZeroDivisionError as b: print('ZeroDivisionError: %s' % b) finally: print('finally>>>>') print('END>>>>') #输出: try>>>> ZeroDivisionError: division by zero finally>>>> END>>>> - 这里修改成'0',最后的输出变成了'ZeroDivisionError'语句块的代码
除了上面说的,如果没有try
语句块没有发生错误时,还可以在except
语句块后面加一个else
语句块,当没有错误发生时,会执行else
语句块,例如:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- try: print('try>>>>') r = 10 / int('2') print('result: %s' % r) except ValueError as a: print('ValueError: %s' % a) except ZeroDivisionError as b: print('ZeroDivisionError: %s' % b) else: print('no error') print('END>>>>') #输出: try>>>> result: 5.0 no error END>>>>
Python的错误其实也是类,所有的错误类型都继承自BaseException,所以在使用except时需要注意,它不但会捕获该类型的错误,还会捕获其子类,例如:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- try: print('try>>>>') r = 10 / int('aaa') print('result: %s' % r) except ValueError as a: print('ValueError: %s' % a) except UnicodeError as b: print('UnicodeError: %s' % b) #输出: try>>>> ValueError: invalid literal for int() with base 10: 'aaa' - 这里的'UnicodeError'语句块永远不会执行,因为'UnicodeError'是'ValueError'的子类,'UnicodeError'的错误会被'ValueError'捕获
Python的所有错误都是从BaseException类派生的,常见的错误类型和继承关系如下:
官网:https://docs.python.org/3/library/exceptions.html#exception-hierarchy BaseException +-- SystemExit +-- KeyboardInterrupt +-- GeneratorExit +-- Exception +-- StopIteration +-- StopAsyncIteration +-- ArithmeticError | +-- FloatingPointError | +-- OverflowError | +-- ZeroDivisionError +-- AssertionError +-- AttributeError +-- BufferError +-- EOFError +-- ImportError | +-- ModuleNotFoundError +-- LookupError | +-- IndexError | +-- KeyError +-- MemoryError +-- NameError | +-- UnboundLocalError +-- OSError | +-- BlockingIOError | +-- ChildProcessError | +-- ConnectionError | | +-- BrokenPipeError | | +-- ConnectionAbortedError | | +-- ConnectionRefusedError | | +-- ConnectionResetError | +-- FileExistsError | +-- FileNotFoundError | +-- InterruptedError | +-- IsADirectoryError | +-- NotADirectoryError | +-- PermissionError | +-- ProcessLookupError | +-- TimeoutError +-- ReferenceError +-- RuntimeError | +-- NotImplementedError | +-- RecursionError +-- SyntaxError | +-- IndentationError | +-- TabError +-- SystemError +-- TypeError +-- ValueError | +-- UnicodeError | +-- UnicodeDecodeError | +-- UnicodeEncodeError | +-- UnicodeTranslateError +-- Warning +-- DeprecationWarning +-- PendingDeprecationWarning +-- RuntimeWarning +-- SyntaxWarning +-- UserWarning +-- FutureWarning +-- ImportWarning +-- UnicodeWarning +-- BytesWarning +-- EncodingWarning +-- ResourceWarning
除了上面说的,使用try...except
捕获错误还有一个好处,就是可以跨越多层调用,例如:
- 使用'main()'调用'bar()','bar()'调用'foo()',这时只要'main()'捕获到错误了,就可以进行错误处理 #!/usr/bin/env python3 # -*- coding: utf-8 -*- def foo(s): return 10 / int(s) def bar(s): return foo(s) * 2 def main(): try: bar('0') except Exception as e: print('Error:', e) finally: print('finally...') main() #输出: Error: division by zero finally...
- 利用这个特性,我们
不需要在每个可能出错的地方捕获错误
,我们只需要在合适的层次捕获错误
即可,这样就可以减少try...except...finally
的数量
- 调用栈
- 如果错误没有被捕获,那么就会一直往上抛,最后被Python解释器捕获,例如:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- def foo(s): return 10 / int(s) def bar(a): return foo(a) * 2 def main(): bar('0') main() #输出: Traceback (most recent call last): File "c:\Users\12488\Desktop\python\pachong.py", line 12, in <module> main() File "c:\Users\12488\Desktop\python\pachong.py", line 10, in main bar('0') File "c:\Users\12488\Desktop\python\pachong.py", line 7, in bar return foo(a) * 2 File "c:\Users\12488\Desktop\python\pachong.py", line 4, in foo return 10 / int(s) ZeroDivisionError: division by zero
出错并不可怕,可怕的是不知道是哪里出错了,而解读错误信息是定位错误的关键步骤,错误信息从上往下看可以得到整个错误的调用函数链,下面我们来解析一些错误信息:
错误信息:
Traceback (most recent call last): File "c:\Users\12488\Desktop\python\pachong.py", line 12, in <module> main() File "c:\Users\12488\Desktop\python\pachong.py", line 10, in main bar('0') File "c:\Users\12488\Desktop\python\pachong.py", line 7, in bar return foo(a) * 2 File "c:\Users\12488\Desktop\python\pachong.py", line 4, in foo return 10 / int(s) ZeroDivisionError: division by zero
第1行:
Traceback (most recent call last): - 错误的跟踪信息
第2—3行:
File "c:\Users\12488\Desktop\python\pachong.py", line 12, in <module> main() - 调用'main()'函数出错了,在代码的12行,但原因是第10行
第4—5行:
File "c:\Users\12488\Desktop\python\pachong.py", line 10, in main bar('0') - 调用'bar('0')'时出错了,在代码的第10行,但原因是第7行
第6—7行:
File "c:\Users\12488\Desktop\python\pachong.py", line 7, in bar return foo(a) * 2 - 原因是因为'return foo(a) * 2'这个语句出错了,但这也不是错误源头
第8—10行:
File "c:\Users\12488\Desktop\python\pachong.py", line 4, in foo return 10 / int(s) ZeroDivisionError: division by zero - 原因是'return 10 / int(s)'这个语句,这是错误的源头,因为下面输出了'ZeroDivisionError: division by zero'
注意:当程序出错的时候,一定要分析错误的调用栈信息,这样可以帮助我们快速的定位错误的位置
- 记录错误
- 如果不捕获错误,自然可以让Python解释器输出错误堆栈,但是这样的话,程序也会被结束
- 我们既然能捕获错误,其实也可以在程序继续执行的情况下,把错误堆栈打印出来,然后分析错误原因
- Python内置的
logging
模块就可以记录错误信息,下面来看案例:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- import logging def foo(s): return 10 / int(s) def bar(a): return foo(a) * 2 def main(): try: bar('0') except Exception as e: logging.exception(e) main() print('END>>>') #输出: ERROR:root:division by zero Traceback (most recent call last): File "c:\Users\12488\Desktop\python\pachong.py", line 13, in main bar('0') File "c:\Users\12488\Desktop\python\pachong.py", line 9, in bar return foo(a) * 2 File "c:\Users\12488\Desktop\python\pachong.py", line 6, in foo return 10 / int(s) ZeroDivisionError: division by zero END>>> - 可以看到同样是出错,但是在程序打印完报错信息后,最后的'print('END>>>')'语句还是正常执行了
- 通过配置,
logging
模块还可以把错误记录到日志文件中,方便进行分析
- 抛出错误
因为错误是类,所以其实捕获一个错误就是捕获到该类的一个实例
错误并不是凭空出现的,而是有意创建并抛出的,Python的内置函数会抛出很多类型的错误,同样,我们自己编写的函数也可以抛出错误
如果要抛出错误,首先要根据需求,可以定义一个错误的类,选择好继承关系,然后使用raise语句抛出错误,例如:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- class FooError(ValueError): pass def test_1(s): num = int(s) if num == 0 : raise FooError('value is %s !!!' % num) return print(10 / num) test_1('0') #输出: Traceback (most recent call last): File "f:\MY_python\Python\test2.py", line 13, in <module> test_1('0') File "f:\MY_python\Python\test2.py", line 10, in test_1 raise FooError('value is %s !!!' % num) __main__.FooError: value is 0 !!! - 可以看到会抛出我们自定义的错误,现在来修改传入的参数 #!/usr/bin/env python3 # -*- coding: utf-8 -*- class FooError(ValueError): pass def test_1(s): num = int(s) if num == 0 : raise FooError('value is %s !!!' % num) return print(10 / num) test_1('2') #输出 5.0
- 从上面的案例可以看到,代码执行后会抛出我们自定义的错误
一般来说只有在必要的时候才会自定义我们自己的错误类型,通常都是选择Python已有的内置错误类型
- 还有一种处理错误的方法,下面来看案例:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- def foo(s): num = int(s) if num == 0 : raise ValueError('value is %s !!' % num) return print(10 / num) def bar(): try: foo('0') except ValueError as e: print('ValueError') raise bar() #输出: ValueError Traceback (most recent call last): File "f:\MY_python\Python\test2.py", line 16, in <module> bar() File "f:\MY_python\Python\test2.py", line 11, in bar foo('0') File "f:\MY_python\Python\test2.py", line 6, in foo raise ValueError('value is %s !!' % num) ValueError: value is 0 !!
可以看到,在bar()函数中,已经捕获了错误,输了ValueError,但是在后面又通过raise语句把foo()函数的错误抛出去了,这样的处理方法非常常见
捕获错误的目的只是记录一下,方便后续解决错误,但是,由于当前函数不知道该怎么处理该错误,所以,最恰当的方式就是把错误往上面抛,让上一层层调用者去处理,最终让顶层调用者处理
raise语句如果不带参数,就会把当前错误原因抛出
在except中,使用raise抛出一个错误,还可以把一种类型的错误转换成另一种类型,例如:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- try: 10 /0 except ZeroDivisionError: raise ValueError('error!!!') #输出: Traceback (most recent call last): File "f:\MY_python\Python\test2.py", line 4, in <module> 10 /0 ZeroDivisionError: division by zero During handling of the above exception, another exception occurred: Traceback (most recent call last): File "f:\MY_python\Python\test2.py", line 6, in <module> raise ValueError('error!!!') ValueError: error!!! - 可以看到'except'使用的错误类型是'ZeroDivisionError',但是最终抛出的错误类型是'ValueError'
注意:
Python内置的try...except...finally用来处理错误十分方便。但是在出错时,会分析错误信息并定位错误发生的代码位置才是最关键也是最重要的
在编写程序时,可以使程序主动抛出错误,让调用者来处理相应的错误。但是,应该在文档中写清楚可能会抛出哪些错误,以及错误产生的原因