Python基础(十一) 异常处理
在程序运行过程中,总会遇到各种各样的错误,有的错误是程序编写有问题造成的,比如本来应该输出整数结果输出了字符串,这样的错误我们通常称之为BUG,BUG是必须修复的。在Python中内置了一套异常处理机制,来帮助我们进行过错误处理,此外我们也需要跟踪程序的执行,查看变量的值是否正确,这个过程称为调试。Python的pdb可以让我们以单步方式执行代码。下面开始今天的内容
一、捕获错误
1、异常介绍
在编程过程中为了增强友好性,在程序出现bug时一般不会讲错误信息显示给用户,而是现实一个提示的页面,通俗的来说就是不让用户看见大黄页。
1
2
3
4
|
try : pass except Exception as ex: pass |
需求:将用户输入的两个数字相加。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#!/usr/bin/env python #-*- coding:utf-8 -*- while True : num1 = input ( 'input num1:' ) num2 = input ( 'input num2:' ) try : num1 = int (num1) num2 = int (num2) except Exception as e: #捕获任何异常 print ( '出现异常,信息如下:' ) print (e) #结果: input num1:test input num2: 1 出现异常,信息如下: invalid literal for int () with base 10 : 'test' |
2、异常种类
python中的异常种类非常多,每个异常专门用于处理某一个项异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
AttributeError #试图访问一个对象没有的树形,比如foo.x,但是foo没有属性x IOError #输入/输出异常;基本上是无法打开文件 ImportError #无法引入模块或包;基本上是路径问题或名称错误 IndentationError #语法错误(的子类) ;代码没有正确对齐 IndexError #下标索引超出序列边界,比如当x只有三个元素,却试图访问x[5] KeyError #试图访问字典里不存在的键 KeyboardInterrupt #Ctrl+C被按下 NameError #使用一个还未被赋予对象的变量 SyntaxError Python #代码非法,代码不能编译(个人认为这是语法错误,写错了) TypeError #传入对象类型与要求的不符合 UnboundLocalError #试图访问一个还未被设置的局部变量,基本上是由于另有一个同名的全局变量,导致你以为正在访问它 ValueError #传入一个调用者不期望的值,即使值的类型是正确的 |
更多异常请参考官网地址:https://docs.python.org/3/library/exceptions.html#exception-hierarchy
下面举几个捕获异常的实例:
捕获IndexError错误:
1
2
3
4
5
6
7
8
9
10
11
|
#!/usr/bin/env python #-*- coding:utf-8 -*- dic = [ "jack" , "eric" ] try : dic[ 10 ] except IndexError as e: print (e) #结果: list index out of range |
捕获KeyError错误:
1
2
3
4
5
6
7
8
9
10
11
|
#!/usr/bin/env python #-*- coding:utf-8 -*- dic = { 'k1' : 'v1' } try : dic[ 'k20' ] except KeyError as e: #捕获KeyError print (e) #结果: 'k20' |
捕获ValueError错误:
1
2
3
4
5
6
7
8
9
10
11
|
#!/usr/bin/env python #-*- coding:utf-8 -*- s1 = 'hello' try : int (s1) except ValueError as e: print (e) #结果: invalid literal for int () with base 10 : 'hello' |
对于上述实例,异常类只能用来处理指定的异常情况,如果非指定异常则无法处理。
1
2
3
4
5
6
7
8
9
10
11
12
|
#!/usr/bin/env python #-*- coding:utf-8 -*- s1 = 'hello' try : int (s1) except IndexError as e: #如果未捕获到异常,程序直接报错 print (e) |
所以,写程序时需要考虑到Try代码块中可能出现的任意异常,这时可能想到我把能想到的错误都预先写好,但这不是最佳的解决办法,在Python中给我们提供了一个捕获万能异常的参数:Exception,它可以捕获任意异常,即:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#!/usr/bin/env python #-*- 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) if __name__ = = '__main__' : main() #结果: Error: division by zero |
接下来你可能要问了,既然有这个万能异常,其他异常是不是就可以忽略了,NO!!对于特殊处理货提醒的异常需要先定义,最后定义Exception来确保程序正常运行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#!/usr/bin/env python #-*- coding:utf-8 -*- s1 = 'hello' try : int (s1) except KeyError as e: print ( '键错误' ) except IndexError as e: print ( '索引错误' ) except Exception as e: print ( '错误' ) #结果: 错误 |
异常还有个更高级的结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
#!/usr/bin/env python #-*- 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) else : #主代码块执行完,不出错执行该步骤 print ( 'else....' ) finally : #无论异常与否,最终执行该步骤 print ( 'finally...' ) if __name__ = = '__main__' : main() #结果: Error: division by zero finally ... |
二、记录错误
如果不捕获错误,自然可以让Python解释器来打印出错误,但程序也被结束了。既然我们能捕获错误,就可以把错误打印出来,然后分析错误原因,同时,让程序继续执行下去。
我们之前介绍过日志模块logging,可以非常容易地记录错误信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#!/usr/bin/env python #-*- coding:utf-8 -*- import logging def foo(s): return 10 / int (s) def bar(s): return foo(s) * 2 def main(): try : bar( '0' ) except Exception as e: logging.exception(e) main() print ( 'END' ) |
执行上面代码同样是出错看,但程序打印完错误信息后继续执行,并正常退出:
1
2
3
4
5
6
7
8
9
10
|
ERROR:root:division by zero Traceback (most recent call last): File "E:/Python_project/SOCKET 代码/my_code.py" , line 15 , in main bar( '0' ) File "E:/Python_project/SOCKET 代码/my_code.py" , line 11 , in bar return foo(s) * 2 File "E:/Python_project/SOCKET 代码/my_code.py" , line 8 , in foo return 10 / int (s) ZeroDivisionError: division by zero END |
通过配置,logging还可以把错误记录到日志文件里,方便事后排查。
三、抛出错误
因为错误是Class,捕获一个错误就是捕获到该class的一个实例。因此,错误并不是凭空产生的,而是有意创建并抛出的。Python的内置函数会抛出很多类型的错误,我们自己编写的函数也可以抛出错误。
如果要抛出错误,首先根据需要,可以定义一个错误的Class,选择好继承关系,然后用raise语句抛出一个错误的实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#!/usr/bin/env python #-*- coding:utf-8 -*- class Foo(ValueError): pass def foo(s): n = int (s) if n = = 0 : raise Foo( 'invalid value:%s' % s) #调用raise,抛出异常 return 10 / n foo( '0' ) |
执行上面的代码,我们可以跟踪到我们定义的错误:
1
2
3
4
5
6
|
Traceback (most recent call last): File "E:/Python_project/SOCKET 代码/my_code.py" , line 13 , in <module> foo( '0' ) File "E:/Python_project/SOCKET 代码/my_code.py" , line 10 , in foo raise Foo( 'invalid value:%s' % s) __main__.Foo: invalid value: 0 |
只有在必要的时候才定义我们自己的错误类型,如果可以选择Python已有的内置的错误类型(比如ValueError,TyprError),尽量使用Python内置的错误类型。
1
2
3
4
5
6
7
8
9
10
11
12
|
class MyException(Exception): def __init__( self , msg): self .message = msg def __str__( self ): return self .message try : raise MyException( '我的异常' ) except MyException as e: print (e) |
四、单元测试
1、如何编写单元测试
首先来介绍一个概念(断言),凡是用print()来辅助查看的地方,都可以用断言(assert)来替代。
单元测试是用来对一个模块、一个函数或者一个类进行正确性检验的测试工作。
比如对内置函数abs(),我们可以编写出一下几个测试用例:
-
输入整数,比如1、1.2、0.99,期待返回值与输入相同;
-
输入负数,比如-1、-1.2、-0.99,期待返回值与输入相反;
-
输入0,期待返回0;
-
输入非数值类型,比如None、[]、{},期待抛出TypeError。
把上面的测试用例放到一个测试模块里,就是一个完整的单元测试。
如果单元测试通过,说明我们测试的这个函数能够正常工作。如果单元测试不通过,要么函数有bug,要么测试条件输入不正确,总之,需要修复使单元测试能够通过。
单元测试通过后有什么意义呢?
如果我们队abs()函数代码做了修改,只需要跑一遍单元测试,如果通过,说明我们的修改不会对abs()函数原有的行为造成影响,如果测试不通过,说明测试不通过,说明我们的修改与原有行为不一致,要么修改代码,要么修改测试。这种以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。在将来修改的时候,可以极大程度地保证该模块行为仍是正确的。
下面我们来编写一个Dict类,这个类的行为和dict一致,但是可以通过属性来访问,用起来就像下面这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#自己定义的代码mydict.py class Dict ( dict ): def __init__( self , * * kw): super ().__init__( * * kw) def __getattr__( self , key): try : return self [key] except KeyError: raise AttributeError(r "'Dict' object has no attribute '%s'" % key) def __setattr__( self , key, value): self [key] = value |
为了编写单元测试,我们需要引入Python自带的unittest模块,编写如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
import unittest from mydict import Dict class TestDict(unittest.TestCase): def test_init( self ): d = Dict (a = 1 , b = 'test' ) self .assertEqual(d.a, 1 ) self .assertEqual(d.b, 'test' ) self .assertTrue( isinstance (d, dict )) def test_key( self ): d = Dict () d[ 'key' ] = 'value' self .assertEqual(d.key, 'value' ) def test_attr( self ): d = Dict () d.key = 'value' self .assertTrue( 'key' in d) self .assertEqual(d[ 'key' ], 'value' ) def test_keyerror( self ): d = Dict () with self .assertRaises(KeyError): value = d[ 'empty' ] def test_attrerror( self ): d = Dict () with self .assertRaises(AttributeError): value = d.empty |
编写单元测试时,我们需要编写一个测试类,从unittest.TestCase继承。
以test开头的方法就是测试方法,不以test开头的方法不被认为是测试方法,测试的时候不会被执行。
对每一类测试都需要编写一个test_xx()方法。由于unittest.TestCase提供了很多内置的条件判断,我们只需要调用这些方法就可以断言输出是否是我们所期望的,最常用的断言就是assertEqual():
1
|
self .assertEqual( abs ( - 1 ), 1 ) #断言函数返回的结果与1相等 |
另一种中烟的断言就是期待抛出指定类型的Error,比如通过d['empty']访问不存在的key时,断言会抛出KeyError:
1
2
|
with self .assertRaises(KeyError): value = d[ 'empty' ] |
而通过d.empty访问不存在的key时,我们期待抛出AttributeError:
1
2
|
with self .assertRaises(AttributeError): value = d.empty |
2、运行单元测试
一旦编写好单元测试,我们就可以运行单元测试。最简单的运行方式是在mydict_test.py的最后加上两行代码:
1
2
3
4
5
6
7
8
9
|
if __name__ = = '__main__' : unittest.main() #执行结果: ..... - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Ran 5 tests in 0.000s OK |
3、setUp与tearDown方法
可以在单元测试中编写两个特殊的setUp()和tearDown()方法,这两个方法会分别在每调用一个测试方法的前后分别被执行。那这两个方法到底有什么用呢?
当我们测试需要连接启动一个数据库,这时就可以在setUp()方法中连接数据库,在tearDown()方法中关闭数据库,这样不必再每个测试方法中重复相同的代码。
1
2
3
4
5
6
7
|
class TestDict(unittest.TestCase): def setUp( self ): print ( 'setUp...' ) def tearDown( self ): print ( 'tearDown...' ) |
可以再次运行测试看看每个测试方法调用前后是否会打印出setUp...和tearDown...。
总结一下,今天我们主要介绍了如何捕获错误信息,记录错误和单元测试,单元测试在我们日后的工作中很是重要,可以检测自己写的程序是否有BUG,单元测试通过了并不意味着程序就没有BUG了,单数不通过程序肯定有BUG。